Let's define a Tags component (a fancy checkbox group).
const Tags = ({ tags, selectedIds, onSelectionChange }) => {
const createClickHandler = (id) => () => {
const newSelectedIds = xor(selectedIds, [id]);
const selectedTags = newSelectedIds.map((id) =>
tags.find((tag) => tag.id === id)
);
onSelectionChange(selectedTags);
};
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style=
onClick={createClickHandler(id)}
>
{text}
</button>
))}
</div>
);
};
This allows us to consume it like this:
export default function App() {
const tags = someUsers.map((user) => ({
id: user.id,
text: user.name,
value: user
}));
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
return (
<div>
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={setSelectedTags}
/>
</div>
);
}
You can test this in https://codesandbox.io/s/musing-goldwasser-nmm13
I believe this is a decent design of a component and its props (the main focus is on the ease of consuming for the other components). We could perhaps remove selectedIds and add a selected flag in the tags prop, however this is beyond the question scope.
My colleague on the other hand insists that this can lead to bugs and should be avoided.
The reasoning is as follows: if we want to update the state we must use appropriate API - setState(oldState => //data manipulation to produce new state) from useState (https://reactjs.org/docs/hooks-reference.html#functional-updates) Since the parent passes the state directly to the children we can't be sure that the child component filters data based on the latest data. Basically, this issue: https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value
His implementation would be something along these lines:
const Tags = ({ tags, selectedIds, onTagClick }) => {
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style=
onClick={() => onTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
In this case, we lift the whole filtering to a parent component
const handleTagClick = (id) =>
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
You can test this in: https://codesandbox.io/s/kind-cdn-j7cg3
or another version:
const Tags = ({ tags, selectedIds, setSelectedIds }) => {
const isSelected = (id) => selectedIds.includes(id);
const handleTagClick = (id) =>
setSelectedIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style=
onClick={() => handleTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
in this case, we leave the filtering to the Tags component however we pass the function which allows modification of state based on old state.
You can test this code in https://codesandbox.io/s/relaxed-leftpad-y13wo
In my opinion, this case is a completely different scenario that React docs never specifically address.
As far as I understand React rendering engine will always ensure that the child nodes get the newest props so a situation where a child component filters (or does other manipulation) with stale data is simply impossible. I would like to quote some docs for this however I haven't found any information on this specific situation.
All I know is:
- with my many years of React experience I have yet to encounter any bugs with my approach
- other 3rd party libraries use the same design
Can someone (with deep React knowledge) provide more insight why I am correct or wrong in this instance?
from Can the child component receive old data for manipulation from parent component?
No comments:
Post a Comment