Kliensoldali webprogramozás
Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu
// functional component
function ColorBox() {
return (
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
)
}
// class component
class ColorBox extends React.Component {
render() {
return (
<div style={{ backgroundColor: `hsl(${random(0, 360)}, 50%, 50%)` }}>
React rendered box, random value: {random(1, 100)}
</div>
)
}
}
props | state |
---|---|
nem módosítható | módosítható |
immutábilis | mutábilis |
kívülről érkezik | belül definiált |
this.props |
this.state |
props |
useState() |
állapotmentes komponens | állapotteli komponens |
Data down, action up
function Card() {
// Component state
const [filterText, setFilterText] = useState("");
const [titles, setTitles] = useState(["title1", "title2", "item3"]);
// Computed values
const filteredTitles = titles.filter((e) => e.includes(filterText));
// Event handlers
const changeFilterText = (newVal) => setFilterText(newVal);
return (
<div className="card text-center">
<CardHeader />
<div className="card-body">
<FilterForm
filterText={filterText}
onFilterTextChange={changeFilterText}
/>
<FilterList list={filteredTitles} />
</div>
<CardFooter list={filteredTitles} />
</div>
);
}
function FilterForm({ filterText, onFilterTextChange }) {
return (
<form className="form-inline">
<label htmlFor="inlineFormInputName2">Filter</label>
<input
value={filterText}
onInput={(e) => onFilterTextChange(e.target.value)}
type="text"
className="form-control m-2"
id="text-filter"
/>
</form>
);
}
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
Strict Mode: kétszer csatolja fel a komponenseket!
function Todos() {
// data
const todos = [
{ title: "todo1", active: true },
{ title: "todo2", active: false },
{ title: "todo3", active: true },
];
// computed/derivated data
const activeTodos = todos.filter(todo => todo.active);
// JSX
return <TodoList list={activeTodos} />;
}
function LocalVariable() {
let n = 3;
const increment = () => { n = n + 1; };
return (
<>
<button onClick={increment}>Increment</button>
<p>N = {n}</p>
</>
);
}
useState
function StateVariable() {
const [n, setN] = useState(0); // let n = 3;
const increment = () => {
setN(n + 1); // n = n + 1;
};
return (
<>
<button onClick={increment}>Increment</button>
<p>N = {n}</p>
</>
);
}
Ha a state megváltozik, akkor van trigger!
function Rerender() {
const [n, setN] = useState(3);
return (
<>
<button onClick={() => setN(3)}>Change to 3</button>
<button onClick={() => setN(5)}>Change to 5</button>
<p>N = {n}</p>
</>
);
}
Komponensenként külön
function StateVariable() {
return (
<>
<Incrementer />
<Incrementer />
</>
);
}
function Incrementer() {
const [n, setN] = useState(0);
const increment = () => {
setN(n + 1);
};
return (
<>
<button onClick={increment}>Increment</button>
<p>N = {n}</p>
</>
);
}
Külső tárolóból lokális változóba olvassuk -> closure
function StateVariable3() {
const [n, setN] = useState(0);
const handleAlertClick = () => {
setTimeout(() => {
alert("You clicked on: " + n);
}, 3000);
};
return (
<>
<button onClick={() => setN(n + 1)}>Increment</button>
<button onClick={handleAlertClick}>Alert</button>
<p>N = {n}</p>
</>
);
}
function StateVariable() {
const [n, setN] = useState(0);
const increment = () => {
setN(n + 1);
setN(n + 1);
setN(n + 1);
};
// ...
}
// State updater function
function StateVariable() {
const [n, setN] = useState(0);
const increment = () => {
setN(n => n + 1);
setN(n => n + 1);
setN(n => n + 1);
};
// ...
}
Mennyi lesz n értéke?
setN(n + 1);
alert(n);
setN(n + 1);
setTimeout(() => {
alert(n);
}, 3000);
setN(n + 10);
setN((n) => n + 1);
setN(n + 10);
setN((n) => n + 1);
setN(42);
DEMO
Mutábilis változásokkor a referencia megmarad
function App() {
const [numbers, setNumbers] = useState([]);
// Wrong
const addNumber = () => {
numbers.push(Math.random())
setNumbers(numbers)
}
return (
<>
<button onClick={addNumber}>Add</button>
<ul>
{numbers.map(n => <li key={n}>{n}</li>)}
</ul>
</>
);
}
Immutábilis változásokkor új struktúra jön létre
function App() {
const [numbers, setNumbers] = useState([]);
// Good
const addNumber = () => setNumbers([...numbers, Math.random()])
// or
const addNumber = () => setNumbers(numbers => [...numbers, Math.random()])
return (
<>
<button onClick={addNumber}>Add</button>
<ul>
{numbers.map(n => <li key={n}>{n}</li>)}
</ul>
</>
);
}
const obj = { a: 1, b: 2 }
// Mutable
// still the same object outside, but the contents have changed
obj.b = 3
// Immutable
const obj2 = Object.assign({}, obj, { b: 3 })
const obj2 = {...obj, b: 3 }
const arr = ['a', 'b']
// Mutable
arr.push('c')
// Immutable
const arr2 = arr.concat('c')
const arr3 = [...arr, 'c']
// or, we can make a copy of the original array:
const arr4 = arr.slice()
// and mutate the copy:
arr4.push('c')
const arr = ['a', 'b', 'c', 'd']
// Mutable
arr[1] = 'q'
// Immutable
const arr2 = [...arr.slice(0, 1), 'q', ...arr.slice(2)]
const arr3 = arr.slice()
arr3[1] = 'q'
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}
const Toggle = () => {
const [on, setOn] = useState(false)
const toggle = () => setOn(!on)
return <Switch on={on} onToggle={toggle} />
}
const Switch = ({on, onToggle}) => (
<div>
<SwitchMessage on={on} />
<SwitchButton onToggle={onToggle} />
</div>
)
const SwitchMessage = ({on}) =>
<div>The button is {on ? 'on' : 'off'}</div>
const SwitchButton = ({onToggle}) =>
<button onClick={onToggle}>Toggle</button>
Object spread operator
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};
const App = () =>
<div>
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
Object spread nem explicit! Nem jó!
function App() {
return <Toolbar theme="dark" />
}
function Toolbar(props) {
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
const ThemeContext = ✒>React.createContext('light')<✒;
function App() {
return (
✒><ThemeContext.Provider value="dark"><✒
<Toolbar />
</ThemeContext.Provider>
)
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
render() {
return <Button theme={✒>theme<✒} />;
}
}
const CounterContext = React.createContext();
function CounterProvider({ children }) {
const [counter, setCounter] = useState(0);
increaseCounter = () => setCounter(counter => counter + 1);
return (
<CounterContext.Provider value={{ counter, increaseCounter }}>
{children}
</CounterContext.Provider>
);
}
function Button() {
const { counter, increaseCounter } = useContext(CounterContext);
return <button onClick={increaseCounter}>Clicked {counter} times</button>;
}
function Application() {
return (
<CounterProvider>
<Button />
</CounterProvider>
)
};
Amikor a prop és a state nem elég
const ref = useRef(0);
// ref
{
current: 0
}
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
DOM API
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Timer API, példa
import { useState, useRef } from 'react';
export default function Stopwatch() {
// This information is used for rendering, so you’ll keep it in state
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
// Since the interval ID is not used for rendering, you can keep it in a ref
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
Minden rerenderkor lefut
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Futás kihagyása
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log('Calling video.play()');
ref.current.play();
} else {
console.log('Calling video.pause()');
ref.current.pause();
}
}, [isPlaying]);
return <video ref={ref} src={src} loop playsInline />;
}
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
Végtelen ciklus
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
function Foo() {
const handleClick = () => console.log('click');
return (
<MyButton onClick={handleClick}>Click</MyButton>
);
}
function Foo() {
const handleClick = useCallback(() => console.log('click'), []);
return (
<MyButton onClick={handleClick}>Click</MyButton>
);
}
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b]
);
import { useState } from 'react';
export function CalculateFactorial() {
const [number, setNumber] = useState(1);
const [inc, setInc] = useState(0);
const factorial = factorialOf(number);
const onChange = event => {
setNumber(Number(event.target.value));
};
const onClick = () => setInc(i => i + 1);
return (
<div>
Factorial of
<input type="number" value={number} onChange={onChange} />
is {factorial}
<button onClick={onClick}>Re-render</button>
</div>
);
}
function factorialOf(n) {
console.log('factorialOf(n) called!');
return n <= 0 ? 1 : n * factorialOf(n - 1);
}
import { useState, useMemo } from 'react';
export function CalculateFactorial() {
const [number, setNumber] = useState(1);
const [inc, setInc] = useState(0);
const factorial = useMemo(() => factorialOf(number), [number]);
const onChange = event => {
setNumber(Number(event.target.value));
};
const onClick = () => setInc(i => i + 1);
return (
<div>
Factorial of
<input type="number" value={number} onChange={onChange} />
is {factorial}
<button onClick={onClick}>Re-render</button>
</div>
);
}
function factorialOf(n) {
console.log('factorialOf(n) called!');
return n <= 0 ? 1 : n * factorialOf(n - 1);
}
import { useState, useEffect } from 'react';
export default function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
import { useState, useEffect } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
const React = (() => {
let hooks = [];
let idx = 0;
function useState(initVal) {
let state = hooks[idx] || initVal;
let _idx = idx;
let setState = newVal => {
hooks[_idx] = newVal;
};
idx++;
return [state, setState];
}
function useRef(val) {
return useState({ current: val })[0];
}
function useEffect(cb, depArray) {
const oldDeps = hooks[idx];
let hasChanged = true;
if (oldDeps) {
hasChanged = depArray.some(
(dep, i) => !Object.is(dep, oldDeps[i])
);
}
if (hasChanged) cb();
hooks[idx] = depArray;
}
function render(Component) {
idx = 0;
const C = Component();
C.render();
return C;
}
return {
useState,
useEffect,
useRef,
render,
};
})();
function Component() {
const [count, setCount] = React.useState(1);
const [text, setText] = React.useState('apple');
return {
render: () => console.log({count, text}),
click: () => setCount(count + 1),
type: word => setText(word),
};
}
var app = React.render(Component);
app.click();
var app = React.render(Component);
app.type("Vue");
var app = React.render(Component);
// ES5: object
var HelloMessage = React.createClass({
render: function() {
return <div>Hello, {this.props.name}!</div>
}
});
// ES6: class
class HelloMessage extends React.Component {
render() {
return <div>Hello, {this.props.name}</div>
}
}
// function
function HelloMessage(props) {
return <div>Hello, {props.name}</div>
}
this
Hook vs Class
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Hook vs Class
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
useState
useEffect
useContext
useRef
useCallback
useMemo