Callbacks en properties
Communicatie tussen verschillende componenten is een van de moeilijkste dingen in React. We raden je aan om altijd eerst zoveel mogelijk te werken met grote componenten en die dan pas op te splitsen alles functioneel is. Zo kan je vaak voorkomen dat je verschillende componenten nodig hebt.
Parent, child en siblings
Vooraleer we gaan bespreken hoe deze componenten gaan kunnen communiceren moeten we even definieren wat parent (ouder), child (kind) en siblings (broers en zussen) zijn.
const ComponentC = () => {
return <div style={StyleC}><p>Component C</p></div>
}
const ComponentB = () => {
return <div style={StyleB}><p>Component B</p></div>
}
const ComponentA = () => {
return (<div style={StyleA}>
<p>Component A</p>
<ComponentB/>
<ComponentC/>
</div>
)
}
We zeggen hier dat ComponentA
de parent is van ComponentB
en ComponentC
. ComponentB
en ComponentC
zijn dan de child components van ComponentA
. ComponentB
en ComponentC
noemen we siblings.
Parent-to-child communicatie
Via props
We hebben tot nu toe geleerd hoe we data kunnen doorgeven aan een component aan de hand van props. Eigenlijk is dit communicatie tussen het Parent en het Child component.
const Child = ({someProperty}: ChildProps) => {
return <div>{someProperty}</div>
}
const Parent = () => {
return <Child someProperty={5}/>
}
Voorbeeld
Stel dat we het uitzicht van de twee child componenten willen aanpassen als we op de parent drukken. Dan kunnen we een property mouseDown
toevoegen aan de child components waar we kunnen doorgeven dat het parent component is ingedrukt. Uiteraard hebben we hier ook een state nodig in het parent component om bij te houden dat het component is ingedrukt of niet.
const ComponentC = ({mouseDown} : ChildProps) => {
let style = mouseDown ? {...StyleC, backgroundColor: "red"} : StyleC;
return <div style={style}><p>Component C</p></div>
}
const ComponentB = ({mouseDown} : ChildProps) => {
let style = mouseDown ? {...StyleB, backgroundColor: "green"} : StyleB;
return <div style={style}><p>Component B</p></div>
}
const ComponentA = () => {
const [mouseDown, setMouseDown] = useState(false);
return (<div style={StyleA} onMouseDown={() => setMouseDown(true)} onMouseUp={() => setMouseDown(false)}>
<p>Component A</p>
<ComponentB mouseDown={mouseDown}/>
<ComponentC mouseDown={mouseDown}/>
</div>
)
}
Child-to-parent communicatie
Via callback functies
Willen we nu kunnen communiceren vanuit de child componenten naar de parent component dan is dit iets complexer. De eenvoudigste manier om dit te doen is door een functie door tegeven vanuit de parent naar het child component. Het child component kan die functie dan gebruiken om te communiceren met de parent. Dit concept noemen we callback functies
interface ChildProps {
callbackFunction: () => void
}
const Child = ({callbackFunction}: ChildProps) => {
return <div><button onClick={callbackFunction}>Click me</button></div>
}
const Parent = () => {
return <Child callbackFunction={() => { console.log("Child was clicked"); }}/>
}
Voorbeeld
We passen het voorbeeld van hiervoor aan zodat de logica nu omgekeerd is. Als we op de child component klikken dan moet de parent component van kleur veranderen. Dus we voorzien een callback onMouse
met een argument down
die we dan doorgeven aan de child componenten. Als er iemand dan duwt op het child component wordt deze onMouse
handler aangeroepen. Deze zorgt ervoor dat de mouseDown
state wordt aangepast afhankelijk of de muis ingedrukt is of niet.
interface ChildProps {
onMouse: (down: boolean) => void
}
const ComponentC = ({onMouse} : ChildProps) => {
return <div onMouseDown={() => onMouse(true)} onMouseUp={() => onMouse(false)} style={StyleC}><p>Component C</p></div>
}
const ComponentB = ({onMouse} : ChildProps) => {
return <div onMouseDown={() => onMouse(true)} onMouseUp={() => onMouse(false)} style={StyleB}><p>Component B</p></div>
}
const ComponentA = () => {
const [mouseDown, setMouseDown] = useState(false);
const mouseHandler = (down: boolean) => {
setMouseDown(down);
}
let style = {...StyleA, ...mouseDown ? { backgroundColor: "blue"} : {}}
return (<div style={style}>
<p>Component A</p>
<ComponentB onMouse={mouseHandler}/>
<ComponentC onMouse={mouseHandler}/>
</div>
)
}
Sibling communicatie
Ook communicatie tussen siblings is mogelijk. Dit gebeurt dan via de parent die de staat van beide componenten bij zal houden. Het is een beetje een combinatie van de bovenstaande twee.
interface ChildProps {
onMouse: (down: boolean) => void;
mouseDown: boolean;
}
const ComponentC = ({mouseDown, onMouse} : ChildProps) => {
let style = mouseDown ? {...StyleC, backgroundColor: "red"} : StyleC;
return <div style={style} onMouseDown={() => onMouse(true)} onMouseUp={() => onMouse(false)}><p>Component C</p></div>
}
const ComponentB = ({mouseDown, onMouse} : ChildProps) => {
let style = mouseDown ? {...StyleB, backgroundColor: "green"} : StyleB;
return <div style={style} onMouseDown={() => onMouse(true)} onMouseUp={() => onMouse(false)}><p>Component B</p></div>
}
const ComponentA = () => {
const [mouseDownB, setMouseDownB] = useState(false);
const [mouseDownC, setMouseDownC] = useState(false);
return (<div style={StyleA}>
<p>Component A</p>
<ComponentB mouseDown={mouseDownC} onMouse={(down) => { setMouseDownB(down)}}/>
<ComponentC mouseDown={mouseDownB} onMouse={(down) => { setMouseDownC(down)}}/>
</div>
)
}
InputView component
We kunnen deze dingen nu toepassen op ons InputView
child component. Schematisch zal dit als volgt uitzien:
interface InputViewProps {
onAddClick: (text: string) => void
}
Vervolgens voegen we deze properties toe aan het InputView component. En zorgen we ervoor dat deze wordt aangeroepen als we op de button klikken. Als argument voor text
gebruiken we hier de waarde van de state.
const InputView = ({onAddClick} : InputViewProps) => {
const [text, setText] = useState<string>('');
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setText(event.target.value);
}
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
console.log(`InputView.handleClick: ${text}`);
onAddClick(text);
}
return (
<>
<input type="text" id="name" onChange={handleChange} value={text}/>
<button onClick={handleClick}>Add</button>
</>
);
}
Nu kan je de functie die verantwoordelijk is voor de onAddClick
af te handelen gewoon doorgeven aan de hand van een property en het argument afprinten op onze console.
const App = () => {
return (
<div>
<InputView onAddClick={(text) => console.log(`App.onAddClick: ${text}`)}/>
</div>
);
}
Uiteraard hadden we deze console.log
ook kunnen uitvoeren in ons InputView
component dus op dit moment lijkt het allemaal een beetje overbodig.
We voorzien nu een lijst in het App component dat een aantal Todo's zal bijhouden. Dit doen we aan de hand van een state. Het InputView
component heeft geen toegang tot die state, dus moet het wel via de callback handler van hierboven gebeuren:
const App = () => {
const [todos, setTodos] = useState<string[]>([]);
return (
<div>
<ul>
{todos.map((todo,i) => <li key={i}>{todo}</li>)}
</ul>
<InputView onAddClick={(text) => setTodos(todos => [...todos, text])}/>
</div>
);
}
SetState functie doorgeven
Hier bestaat ook nog een andere variant voor die we kunnen gebruiken. We zouden ook de setter van de state en de waarde van de state zelf kunnen doorgeven als properties aan de InputView
zodat deze toch toegang heeft tot deze functie (en waarde):
interface InputViewProps {
todos: string[];
setTodos: (todos: string[]) => void
}
const InputView = ({todos, setTodos} : InputViewProps) => {
const [text, setText] = useState<string>('');
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setText(event.target.value);
}
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
console.log(`InputView: handleClick(${text})`);
setTodos([...todos, text])
}
return (
<>
<input type="text" id="name" onChange={handleChange} value={text}/>
<button onClick={handleClick}>Add</button>
</>
);
}
const App = () => {
const [todos, setTodos] = useState<string[]>([]);
return (
<div>
<ul>
{todos.map((todo,i) => <li key={i}>{todo}</li>)}
</ul>
<InputView setTodos={setTodos} todos={todos}/>
</div>
);
}