Using React hooks, how can I update an object that is being passed to a child via props? - arrays

The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}

Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;

Related

How to let child access the parent data( an array of object), while having the child ability to update them

I have two components in my project.
One is App.jsx
One is Child.jsx
In App, there is a state holding array of child state objects. All create, manage, and update of the child state is through a set function from parent.
So, Child componment doesnt have its own state. For some reason, it is my intention not to have child own state, because it matters.
However, at some points, I found it that passing data into child would be hard to manage.
Question:
So, is there a way that let the child to access the data from parent by themselves not by passing down, while having them be able to update the state like my code.
People say useContext may work, but I dont quite see how.
A example to illustrate would be prefect for the improvement.
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.12/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {StrictMode, useState} = React;
function getInitialChildState () {
return {
hidden: false,
id: window.crypto.randomUUID(),
text: '',
};
}
function Child ({text, setText}) {
return (
<div className="vertical">
<div>{text ? text : 'Empty 👀'}</div>
<input
type="text"
onChange={ev => setText(ev.target.value)}
value={text}
/>
</div>
);
}
function ChildListItem ({state, updateState}) {
const toggleHidden = () => updateState({hidden: !state.hidden});
const setText = (text) => updateState({text});
return (
<li className="vertical">
<button onClick={toggleHidden}>{
state.hidden
? 'Show'
: 'Hide'
} child</button>
{
state.hidden
? null
: <Child text={state.text} setText={setText} />
}
</li>
);
}
function App () {
// Array of child states:
const [childStates, setChildStates] = useState([]);
// Append a new child state to the end of the states array:
const addChild = () => setChildStates(arr => [...arr, getInitialChildState()]);
// Returns a function that allows updating a specific child's state
// based on its ID:
const createChildStateUpdateFn = (id) => (updatedChildState) => {
setChildStates(states => {
const childIndex = states.findIndex(state => state.id === id);
// If the ID was not found, just return the original state (don't update):
if (childIndex === -1) return states;
// Create a shallow copy of the states array:
const statesCopy = [...states];
// Get an object reference to the targeted child state:
const childState = statesCopy[childIndex];
// Replace the child state object in the array copy with a NEW object
// that includes all of the original properties and merges in all of the
// updated properties:
statesCopy[childIndex] = {...childState, ...updatedChildState};
// Return the array copy of the child states:
return statesCopy;
});
};
return (
<div>
<h1>Parent</h1>
<button onClick={addChild}>Add child</button>
<ul className="vertical">
{
childStates.map(state => (
<ChildListItem
// Every list item needs a unique key:
key={state.id}
state={state}
// Create a function for updating a child's state
// without needing its ID:
updateState={createChildStateUpdateFn(state.id)}
/>
))
}
</ul>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root'));
reactRoot.render(
<StrictMode>
<App />
</StrictMode>
);
</script>
Usually context in react is used for a global things like themes and authentication. But you can use it for actions too.
const AppContext = createContext();
In App:
const getChildState = (id) => ...
const updatedChildState = (id, updatedChildState) =>
<AppContext.Provider value={{ getChildState, updatedChildState }}>...
In ChildListItem:
const { getChildState, updatedChildState } = useContext(AppContext);
const state = getChildState(id);
const setText = (text) => updatedChildState(id, { text });
You need to pass down the id anyway so ChildListItem know what to get and what to update:
<ChildListItem key={state.id} id={state.id} />
Working example
Update
Regarding your question about the theme and authentication examples let's first cite the documentation:
In a typical React application, data is passed top-down (parent to
child) via props, but such usage can be cumbersome for certain types
of props (e.g. locale preference, UI theme) that are required by many
components within an application. Context provides a way to share
values like these between components without having to explicitly pass
a prop through every level of the tree.
Examples:
Material UI uses ThemeProvider to pass down theme object. Thus all components can access the palette, typography etc.
Many apps uses context to pass down information about a currently logged in user. So all components can render accordingly.
You could try jotai atoms
App.jsx
import { atom, useAtom } from 'jotai'
export const itemAtom = atom('')
export const App = () => {
const [item] = useAtom(itemAtom)
<p>{item}</p>
...
}
Child.jsx
export const Child = () => {
const [, setItem] = useAtom(itemAtom)
<input onChange={(e) => setItem(e.value)} />
...
}

Making the state of a component affect the rendering of a sibling when components are rendered iteratively

I have the following code:
export default function Parent() {
const children1 = someArrayWithSeveralElements.map(foo => <SomeView />);
const children2 = someArrayWithSeveralElements.map(foo => <SomeCheckbox />);
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
For a given element foo, there is a SomeView component that is conditionally rendered based on the state of a SomeCheckbox. I'm having trouble figuring out a way to have the state from the checkbox affect the rendering of the sibling view component.
Normally the solution would be to just declare the state hook in the parent component and pass them down to each child, but since the siblings are rendered via foreach loops it's impossible to do so.
My current solution is to also generate the state hooks for each foo in a loop as well, but that feels a bit hacky since it's better to avoid creating hooks inside of loops (it's worth nothing that someArrayWithSeveralElements is not intended to change after mounting).
Is there a more elegant alternative to solve this?
The solution is what you side, you need to create a state in the parent component and pass it to the children. and this will work for single component or bunch of them, the difference is just simple: use array or object as state.
const [checkboxesStatus, setCheckboxesStatus] = useState({// fill initial data});
const children1 = someArrayWithSeveralElements.map(foo =>
<SomeView
visibile={checkBoxesStatus[foo.id]}
/>);
const children2 = someArrayWithSeveralElements.map(foo =>
<SomeCheckbox
checked={checkBoxesStatus[foo.id]}
onChange={// set new value to foo.id key}
/>)
export default function Parent() {
const [states, setStates] = React.useState([]);
const children1 = someArrayWithSeveralElements.map((foo, i) => <SomeView state={states[i]} />);
const children2 = someArrayWithSeveralElements.map((foo, i) => {
const onStateChange = (state) => {
setStates(oldStates => {
const newStates = [...(oldStates || [])]
newStates[i] = state;
return newStates;
})
}
return <SomeCheckbox state={states[i]} onStateChange={onStateChange} />;
});
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
Use states in the parent componet.
Note: the element of states may be undefined.

React - UseEffect not re-rendering with new data?

This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}

State changes from parent to children not reflected to TextField in React Hook

I pass a component (C) as props to a Child component (B) inside a Parent component (A). State of A is also passed to C and mapped to C's state. But when I update A's state and B's state accordingly, state of C does not update.
My code looks like this: (import statements are omitted)
const Parent = (props) => {
.............(other state)
const [info, setInfo] = React.useState(props.info);
const handleDataChanged = (d) => { setInfo(d); }
return (
<div>
........(other stuffs)
<MyModal
..........(other props)
body={ <MyComp data={ info } updateData={ handleDataChanged } /> }
/>
</div>
);
}
const MyModal = (props) => {
..........(other state)
const [content, setContent] = React.useState(props.body);
React.useEffect(() => { setContent(props.body); }, [props]);
return (
<Modal ...>
<div>{ content }</div>
</Modal>
);
}
const MyComp = (props) => {
const [data, setData] = React.useState(props.data);
React.useEffect(() => { setData(props.data); }, [props]);
return (
data && <TextField value={ data.name }
onChange={ e => {
let d = data;
d.name = e.target.value;
props.updateData(d); }} />
);
}
When I type something in the TextField, I see Parent's info changed. The useEffect of MyModal is not fired. And data in MyComp is not updated.
Update: After more checking the above code and the solution below, the problem is still, but I see that data in MyComp does get changes from Parent, but the TextField does not reflect it.
Someone please show me how can I update data from MyComp and reflect it to Parent. Many thanks!
Practically, it looks like you are trying to recreate the children api https://reactjs.org/docs/react-api.html#reactchildren.
Much easier if you use props.children to compose your components instead of passing props up and down.
const MyModal = (props) => {
...(other state)
return (
<Modal>
<div>{ props.children }</div>
</Modal>
);
}
Then you can handle functionality directly in the parent without having to map props to state (which is strongly discouraged)...
const Parent = (props) => {
...(other state)
const [info, setInfo] = React.useState(props.info);
const handleDataChanged = d => setInfo(d);
return (
<div>
...(other stuffs)
<MyModal {...props}>
<MyComp data={ info } updateData={ handleDataChanged } />
</MyModal>
</div>
);
}
The upside of this approach is that there is much less overhead. rather than passing State A to C and mapping to C's state, you can just do everything from State A (the parent component). No mapping needed, you have one source of truth for state and its easier to think about and build on.
Alternatively, if you want to stick to your current approach then just remove React.useEffect(() => { setContent(props.body); }, [props]); in MyModal and map props directly like so
<Modal>
<div>{ props.body }</div>
</Modal>
The real problem with my code is that: React Hook does not have an idea whether a specific property or element in a state object has changed or not. It only knows if the whole object has been changed.
For example: if you have an array of 3 elements or a Json object in your state. If one element in the array changes, or one property in the Json object changes, React Hook will identiy them unchanged.
Therefore to actually broadcast the change, you must deep clone your object to a copy, then set that copy back to your state. To do this, I use lodash to make a deep clone.
Ref: https://dev.to/karthick3018/common-mistake-done-while-using-react-hooks-1foj
So the code should be:
In MyComp:
onChange={e => { let d = _.cloneDeep(data); d.name = e.target.value; props.handleChange(d) }}
In Parent:
const handleChange = (data) => {
let d = _.cloneDeep(data);
setInfo(d);
}
Then pass the handleChange as delegate to MyComp as normal.

Using state setter as prop with react hooks

I'm trying to understand if passing the setter from useState is an issue or not.
In this example, my child component receives both the state and the setter to change it.
export const Search = () => {
const [keywords, setKeywords] = useState('');
return (
<Fragment>
<KeywordFilter
keywords={keywords}
setKeywords={setKeywords}
/>
</Fragment>
);
};
then on the child I have something like:
export const KeywordFilter: ({ keywords, setKeywords }) => {
const handleSearch = (newKeywords) => {
setKeywords(newKeywords)
};
return (
<div>
<span>{keywords}</span>
<input value={keywords} onChange={handleSearch} />
</div>
);
};
My question is, should I have a callback function on the parent to setKeywords or is it ok to pass setKeywords and call it from the child?
There's no need to create an addition function just to forward values to setKeywords, unless you want to do something with those values before hand. For example, maybe you're paranoid that the child components might send you bad data, you could do:
const [keywords, setKeywords] = useState('');
const gatedSetKeywords = useCallback((value) => {
if (typeof value !== 'string') {
console.error('Alex, you wrote another bug!');
return;
}
setKeywords(value);
}, []);
// ...
<KeywordFilter
keywords={keywords}
setKeywords={gatedSetKeywords}
/>
But most of the time you won't need to do anything like that, so passing setKeywords itself is fine.
why not?
A setter of state is just a function value from prop's view. And the call time can be anytime as long as the relative component is live.

Resources