Re-render a sibling component using hooks - javascript

I am new to react and I'm trying to get the one component to re-render from another component.
Here's my code:
const Parent = () => {
return (
<div>
<Child1 />
<Child2 />
</div>
)
}
What I intend to do is update Child1 when there is some trigger from Child2.
One way I can think of is to get the parent component to re-render so both Child1 and Child2 will be updated. I tried to do this by lifting the state but it doesn't seem to re-render each of the child components. Here's the code
const Parent = () => {
const [value, setValue] = useState('')
const handlePost = (newValue) => {
setValue(newValue)
}
return (
<div>
<Child1 />
<Child2 onPost={handlePost} />
</div>
)
}
const Child2 = (props) => {
// This function is executed when there is a trigger.
// In this case, when a post request is made to the server
const onPost() => {
props.handlePost('new value')
}
}
Edit:
The reason why the component(s) needs to be re-rendered is because they are making changes to the API and these changes need to be reflected on the screen. It has nothing to do with any state variables.

Your question is an XY problem. In the example given it does not make sense that Child1 rerenders cause there is no need for it. From the comments your real problem is that you update one API, which is supposed to change the response of another API. If you however already know how the response will change, and that it will change, this can be reflected in one state that changes for both API calls:
function useEntries() {
const [entries, setEntries] = useState([]);
useEffect(() => {
setEntries(getEntries());
}, []);
function addEntry(entry) {
postEntry(entry);
setEntries(prev => [...prev, entry]);
}
return { entries, addEntry };
}
function Parent() {
const { entries, addEntry } = useEntries();
return <>
<Entries entries={entries} />
<AddEntry addEntry={addEntry} />
</>;
}

From the comments in the post, it sounds like you have Child1 presenting results of a GET request (being done in Child1). Child2 can add or modify that state on the server with some kind of request and you want to trigger a re-render in order to make Child1 refresh the state.
The general problem is, that children should only re-render if props or their used contexts change. I see two options how to approach this:
Lift the handling of the requests up into the parent. Put the results of the request as props into the child component you want to refresh.
Make the sibling aware of the request having to reload by setting it to "dirty" in some way. Either through context or routing state around through the parent.
Usually it's best to go with option 1 if the components are not too deeply nested. It could look like this:
const Parent = () => {
const [posts, setPosts] = useState([]);
const fetchNewestPosts = useCallback(async () => {
const fetched = await fetchPosts();
setPosts(fetched);
}, [fetchPosts, setPosts]);
const handleSubmit = useCallback(async (event) => {
const newPost = getValuesFromSubmitEvent(event);
await sendNewPost(newPost);
// you could even set the posts here to what you think the
// send request will result in (see Jonas Wilms answer), like
// setPosts(posts => [newPost, ...posts]);
await fetchNewestPosts();
}, [fetchNewestPosts, getValuesFromSubmitEvent, sendNewPost]);
useEffect(() => {
fetchNewestPosts();
}, [fetchNewestPosts]);
return (
<div>
<Child1 posts={posts} />
<Child2 submitNewPost={submitNewPost} />
</div>
);
);
const Child1 = ({posts}) => {
return (
<ul>{posts.map(post => <li key={post.id}>post.title</li>)}</ul>
);
);
const Child2 = ({submitNewPost}) => {
return (
<form onSubmit={submitNewPost}>...</form>
);
);
As a nice side-effect, Child1 and Child2 now need a lot less logic and can be styled independently of the fetchPosts and sendNewPost functions.

Ciao, lets say that Child1 must be re-rendered on handlePost. Your parent component will be:
const Parent= () => {
const [value, setValue] = useState('')
const [rerender, setrerender] = useState(false)
const handlePost = (newValue) => {
setValue(newValue);
let setrerender_temp = rerender;
setrerender(!setrerender_temp);
}
return (
<div>
<Child1 rerender={rerender} />
<Child2 onPost={handlePost} />
</div>
)
}
Then, in your Child1 component:
import React, { useReducer, useEffect } from 'react';
...
export default function Child1(props) {
const [,forceRender] = useReducer((s) => s+1, 0);
useEffect(() => forceRender(), [props.rerender]);
...
}

Related

How to prevent setting a state variable to another state variable?

I have a controlled component that I call Note. I want its default value to be equal to the selected note (which is set in App.js and passed through as a prop). It seems redundant/bad practice. Here's my code, simplified to the relevant parts. How can I set the default value of textarea to be equal to another state variable?
Edit: Forgot to mention that selectedNote is changed in another component. It works for the state set in useEffect but not for the updates.
App.js
function App(){
const [selectedNote, setSelectedNote] = useState("")
useEffect(() => {
async function fetchData(){
let req = await fetch("http://localhost:9292/notes");
let res = await req.json();
setSelectedNote(res[0])
}
fetchData()
},[])
return (
<Note selectedNote={selectedNote.body}/>
)
}
Note.js
function Note({selectedNote}) {
const [editValue, setEditValue] = useState(selectedNote)
return (
<form>
<textarea value={editValue} onChange={handleChange}>
</textarea>
</form>
)
}
(To clarify, I have no issues if I write const [editValue, setEditValue] = useState("testing123") or some other string)
So ideally you want to lift state up so that the parent component manages the state updates, and the Notes component is as dumb as possible.
In this example the data is loaded into state, and then the notes are built, only receiving an id, some body text which will be their value, and an onChange handler.
When the text is changed, the state is copied, the object in the array (defined by the id) updated, and the new array pushed back into state.
const { useEffect, useState } = React;
const json = '[{"id":1,"body":"Note1"},{"id":2,"body":"Note2"},{"id":3,"body":"Note3"}]';
function mockApi() {
return new Promise(res => {
setTimeout(() => res(json), 2000);
});
}
function Example() {
const [ notes, setNotes ] = useState([]);
const [ selectedNote, setSelectedNote ] = useState(null);
useEffect(() => {
mockApi()
.then(res => JSON.parse(res))
.then(data => setNotes(data));
}, []);
function handleChange(e) {
const { value, dataset: { id } } = e.target;
const copy = [...notes];
copy[id - 1].body = value;
setNotes(copy);
}
function handleClick() {
console.log(JSON.stringify(notes));
}
if (!notes.length) return 'Loading';
return (
<div>
{notes.map(note => {
return (
<Note
key={note.id}
id={note.id}
body={note.body}
handleChange={handleChange}
/>
)
})}
<button onClick={handleClick}>
View state
</button>
</div>
);
}
function Note({ id, body, handleChange }) {
return (
<textarea
data-id={id}
value={body}
onChange={handleChange}
/>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can provide a function to useState that will only be invoked once, when the component renders. Use that function to copy the prop value into the Note's private state.
const [editValue, setEditValue] = useState(() => selectedNote)
You may have other problems, such as the prop value being blank on initial render, but this is still usually the most straightforward way to initialize a private state var based on a prop.
If it turns out that blank-initial-state is an insurmountable problem, then you may instead need to set up a useEffect that updates the private state when the prop value changes to a satisfactory value. Something like this:
const [editValue, setEditValue] = useState()
React.useEffect(() => {
// only update state if old is blank & new is not
if(!editValue && selectedNote) setEditValue(selectedNote)
}, [selectedNote])

How do i pass data up from a child web component to it's direct parent element

I need to basically pass a value down to my web component, I will then do some random thing to it, now I want to pass that new value back up to my React Comnponent. How do I do that?
function MyReactcomp() {
const [state, setState] = useState("Justin is cool");
return (
<div className="Wrapper">
<customweb-component state={state} />
</div>
);
}
Now inside the customweb-component, I will change state to "No your not!!". How can I pass this value back up from my web component to
my Rea t Copmponent? I know I can pass a function down because you can only pass strings down
Instead of querying for DOM, you should use React's Ref as shown below:
function MyReactcomp() {
const [state, setState] = useState("Justin is cool");
// Hold the web component reference here
const myWebComp = useRef();
useEffect(() => {
myWebComp.current?.addEventListener('tab-select', (event) => {
setState(Object.keys(adminTabs[event.detail.tabIndex]));
});
}, []);
return (
<div className="Wrapper">
<customweb-component ref={myWebComp} state={state} />
</div>
);
}
And, you if you need to observe for changes in your referenced element, then you can use plain useState to hold reference to the element:
function MyReactcomp() {
const [state, setState] = useState("Justin is cool");
// Hold the web component reference here
const [webComp, setWebComp] = useState(null);
useEffect(() => {
webComp?.addEventListener('tab-select', (event) => {
setState(Object.keys(adminTabs[event.detail.tabIndex]));
});
}, [webComp]);
return (
<div className="Wrapper">
<customweb-component ref={setWebComp} state={state} />
</div>
);
}
If you find doing this too many times, you can abstract this behavior into custom hooks. For example:
function useWebCompProp(ref, initialValue) {
const [state, setState] = useState(initialValue);
useEffect(() => {
ref.current?.addEventListener('onState', (event) => {
// Update the state here
setState(event.detail);
});
}, []);
const setter = (newState) => {
setState(newState);
ref.current?.state = newState;
};
return [state, setter];
}
function useWebCompEvent(eventName, callback) {
// Hold the web component reference here
const myWebComp = useRef();
useEffect(() => {
myWebComp.current?.addEventListener(eventName, callback);
}, []);
return myWebComp;
}
function MyReactcomp() {
const myWebComp = useWebCompEvent(eventName, (event) => {
setState(Object.keys(adminTabs[event.detail.tabIndex]));
});
const [state, setState] = useWebCompProp(myWebComp, "Justin is cool");
return (
<div className="Wrapper">
<customweb-component ref={myWebComp} />
</div>
);
}
I figured out that you need to attach a custom event handler to an element in your Lit-html template. You can then use getElementById or something to that effect and listen for the event in the parent component.
This is a combination of my answer and Harshal Patil's he took into account that using Reacts built-in features for manipulating the DOM is preferable to manipulating it directly.
import React, { useEffect, useRef } from 'react';
function MyReactcomp() {
const [state, setState] = useState("Justin is cool");
const webCompRef = useRef();
useEffect(() => {
webCompRef.addEventListener('tab-select', (event) => {
setState("No he is not!!")
})
}, []}
return (
<div className="Wrapper">
<customweb-component ref={webCompRef} state={state} />
</div>
);
}
This pattern will allow you to pass your data down through the web components attributes. You can then listen for your native or custom events. This is cool because now components can cross frameworks and libraries, if one team is using React and the other is using Angular they can share components and keep there UI in sync.
In react, the transfer of data is 1-way only, i.e., from Parent-to-Child
However, if you need to have a handler in the parent element for some data change in the child element, you can use Lifting Up State
This could help achieving what you need.
If not, please provide more details about the actual case scenario

Prevent re-rendering in ReactJS of all childs on onChange event

I am setupping a simple dashboard to challeging my self with ReactJS, but I have some issues preventing useless re-rendering.
I have a root component called App where I fetch some data.
const App = () => {
const [data, setData] = useState(null);
const [list1, setList1] = useState(null);
const [list2, setList2] = useState(null);
const [list3, setList3] = useState(null);
const [list4, setList4] = useState(null);
useEffect(() => {
const fetchData = fetchDataInSomeWay();
const fetchedData = getData(fetchData);
const list1Data = getList1(fetchData);
setList1(list1Data);
setData(fetchedData);
});
...
{ data !== null
&& (
<Parent
data={data}
list1={list1}
list2={list2}
list3={list3}
list4={list4}
/>
};
Then I setup a Parent component where I created some Select component and other elements which depend on the values ​​selected by select.
I have a Select element for each list state created with useState();
const Google = ({
data,
list1,
list2,
list3,
list4,
}) => {
const [typeValue, setTypeValue] = useState('someValue');
const [list1Value, setList1Value] = useState(list1[0]);
const [list2Value, setList2Value] = useState(list2[0]);
const [list3Value, setList3Value] = useState(list3[0]);
const [list4Value, setList4Value] = useState(list4[0]);
const onChangeSelectTypeValue = (value) => {
setTypeValue(value);
};
...
const selectTypeValueElement = (
<SelectElement
select={selectType}
value={[typeValue]}
onChangeValue={onChangeSelectTypeValue}
values={list1Value}
/>
);
...
<div className="interactionHeaderChart">
{ selectTypeValueElement }
...
</div>
};
Then I have a Select element where I do not store a state, but where option selected is passed to Parent compoment.
const SelectElement = ({
select, value, values, onChangeValue,
}) => {
...
<Select
...
value={value[0]}
onChange={onChangeValue}
>
...
};
Now when I select some option from one Select, state of Parent change and all Childs re-render, all Selects components and also other components which depend on the values ​​selected by select.
Can I prevent all Select components from re-rendering? Can I avoid to re-render all other components which does not depend on the values of option selected?
The fact that the state has changed from the onChange function and not from useEffect() is confusing me and I can not understand how to solve it.
Thanks.
You should look into shouldComponentUpdate:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Usually, in order to use this with your SelectElement component you will first have to convert it into a Class. You can then add the shouldComponentUpdate function to it and check the previous and next props are the same or not. If they are the same, don't update.
However, if your props are not complex objects, you can actually just recreate your SelectElement as a PureComponent. This will automatically check the props and will not re-render if they're the same.
e.g.
class SelectElement extends React.PureComponet {...
you can use memo to avoid re rendering.
Way 1:
const NestedComponent = () => {
return (
<div>
ContainerComponent
</div>
);
};
export default React.memo(NestedComponent);
Way 2:
function ParentComponent(a, b) {
const childComponent = React.useMemo(() => <ChildComponent posts={a} />, [a]);
return (
<>
{childComponent}
</>
)
}

Refresh specific component in React

I'm using functional component in React and i'm trying to reload the component after button is clicked.
import { useCallback, useState } from 'react';
const ProfileLayout = () => {
const [reload, setReload] = useState(false);
const onButtonClick = useCallback(() => {
setReload(true);
}, []);
return (
{reload && (
<ProfileDetails />
)}
<Button onClick={onButtonClick} />
);
};
export default ProfileLayout;
I'm not able to see again the component after page loaded.
Note: I don't want to use window.location.reload();
Note: I don't want to use window.location.reload();
That is good because that is not the correct way to do this in React. Forcing the browser to reload will lose all of your component or global state.
The correct way to force a component to render is to call this.setState() or call this.forceUpdate().
If you need to force the refresh, then better use a number than a boolean.
const ProfileLayout = () => {
const [reload, setReload] = useState(0);
const onButtonClick = useCallback(() => {
setReload(p => p+1);
}, []);
return (
{Boolean(reload) && (
<ProfileDetails />
)}
);
};
What do you mean by reloading the component? You want to re-render it or you want to make the component fetch the data again? Like "refresh"?
Anyways the way your component is coded the <ProfileDetails /> component will not show up on the first render since you are doing reload && <ProfileDetails />, but reload is initially false. When you click the button then ProfileDetails will appear, but another click on the button won't have any effect since reload is already set to true.
If you want to refresh the data the component uses, then you need to implement a callback that triggers the data fetching.
Edit after clarification by author
const ProfileContainer = (props) => {
// initialProfile is the profile data that you need for your component. If it came from another component, then you can set it when the state is first initialized.
const [ profile, setProfile ] = useState(props.initialProfile);
const loadProfile = useCallback( async () => {
// fetch data from server
const p = await fetch('yourapi.com/profile'); // example
setProfile(p);
}
return (<><ProfileDetails profile={profile} /> <button onClick={loadProfile} /></>)
}
Alternate approach to load the data within the component
const ProfileContainer = (props) => {
const [ profile, setProfile ] = useState(null);
const loadProfile = useCallback( async () => {
// fetch data from server
const p = await fetch('yourapi.com/profile'); // example
setProfile(p);
}
useEffect(() => loadProfile(), []); // Empty dependency so the effect only runs once when component loads.
return (<>
{ /* needed since profile is initially null */
profile && <ProfileDetails profile={profile} />
}
<button onClick={loadProfile} />
</>);
};

Replace parent state with state of child. Button in parent

EDIT: See the comment of O.o for the explanation of the answer and the variant in case you are using classes.
I've come across to something and I can't find the solution.
I have 4 components in my web app:
Parent
child_1
child_2
child_3
I have a button on the Parent, and different forms (with inputs, checkboxes and radiobuttons) at the children.
Each child has his own button that executes several functions, some calculations, and updates the corresponding states. (No states are passed through parent and child).
I need to replace the three buttons of the children with the parent button.
Is there a way that I can execute the functions at the three children from the parent button and retrieve the results? (the results are one state:value per child.)
function Child1(props) {
const [value, setValue] = useState("");
useEffect(() => {
calculate();
}, [props.flag]);
calculate() {
//blah blah
}
onChange(e) {
setValue(e.target.value);
props.onChange(e.target.value); // update the state in the parent component
}
return (
<input value={value} onChange={(e) => onChange(e)} />
);
}
function Parent(props) {
const [flag, setFlag] = useState(false);
const [child1Value, setChild1Value] = useState("");
return (
<div>
<Child1 flag={flag} onChange={(value) => setChild1Value(value)}/>
<button onClick={() => setFlag(!flag)} />
</div>
);
}
I didn't test this but hope this helps you. And lemme know if there is an issue.
Try the following:
create refs using useRef for child form components.
for functional components, in order for the parent to access the child's methods, you need to use forwardRef
using the ref, call child component functions on click of parent submit button (using ref.current.methodName)
See the example code. I have tested it on my local, it is working ok.
Parent
import React, { Fragment, useState, useRef } from "react";
import ChildForm1 from "./ChildForm1";
const Parent = props => {
const [form1Data, setFormData] = useState({});//use your own data structure..
const child1Ref = useRef();
// const child2Ref = useRef(); // for 2nd Child Form...
const submitHandler = e => {
e.preventDefault();
// execute childForm1's function
child1Ref.current.someCalculations();
// execute childForm2's function
// finally do whatever you want with formData
console.log("form submitted");
};
const notifyCalcResult = (calcResult) => {
// update state based on calcResult
console.log('calcResult', calcResult);
};
const handleChildFormChange = data => {
setFormData(prev => ({ ...prev, ...data }));
};
return (
<Fragment>
<h1 className="large text-primary">Parent Child demo</h1>
<div>
<ChildForm1
notifyCalcResult={notifyCalcResult}
ref={child1Ref}
handleChange={handleChildFormChange} />
{/*{do the same for ChildForm2 and so on...}*/}
<button onClick={submitHandler}>Final Submit</button>
</div>
</Fragment>
);
};
export default Parent;
ChildFormComponent
import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react";
const ChildForm1 = ({ handleChange, notifyCalcResult }, ref) => {
const [name, setName] = useState("");
const [calcResult, setCalcResult] = useState([]);
const someCalculations = () => {
let result = ["lot_of_data"];
// major calculations goes here..
// result = doMajorCalc();
setCalcResult(result);
};
useImperativeHandle(ref, () => ({ someCalculations }));
useEffect(() => {
// notifiy parent
notifyCalcResult(calcResult);
}, [calcResult]);
return (
<form className="form">
<div className="form-group">
<input
value={name}// //TODO: handle this...
onChange={() => handleChange(name)}//TODO: notify the value back to parent
type="text"
placeholder="Enter Name"
/>
</div>
</form>
);
};
export default forwardRef(ChildForm1);
Also as a best practice, consider to maintain state and functions in the parent component as much as possible and pass the required values/methods to the child as props.

Categories