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.
Related
I was trying to access Parent state when a function is called from Child component, for that created a function in Parent component and passed it to Child, issue is I am not able to access the state completely.
for example on button click I add a new input field and a delete button, suppose I added 10 input fields, and added all of them in state array, but when i click delete button of second input field, the count I get from state is 1, similar if I click 5th delete button i get count as 4 and it only show me 4 items in state, but it has 10 items
Here is an example link https://codesandbox.io/s/add-react-component-onclick-forked-t2i0ll?file=/src/index.js:0-869
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Input = ({ deleteRow, position }) => {
const handleDelete = () => deleteRow(position);
return (
<>
<input placeholder="Your input here" />
<button onClick={handleDelete}>Delete</button>
</>
);
};
const Form = () => {
const [inputList, setInputList] = useState([]);
const onDeleteRow = (position) => {
console.log("inputCount", inputList);
};
const onAddBtnClick = (event) => {
setInputList(
inputList.concat(
<Input
key={inputList.length}
position={inputList.length}
deleteRow={onDeleteRow}
/>
)
);
};
return (
<div>
<button onClick={onAddBtnClick}>Add input</button>
{inputList}
</div>
);
};
ReactDOM.render(<Form />, document.getElementById("form"));
It's called a stale closure.
You can avoid that by not storing react elements inside the state.
Example:
const Form = () => {
const [inputList, setInputList] = useState([]);
const onDeleteRow = (position) => {
console.log("inputCount", inputList);
};
const onAddBtnClick = (event) => {
setInputList([...inputList, inputList.length])
);
};
return (
<div>
<button onClick={onAddBtnClick}>Add input</button>
{inputList.map(item => (
<Input
key={item}
position={item}
deleteRow={onDeleteRow}
/>
)}
</div>
);
};
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
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]);
...
}
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}
</>
)
}
So I've been at this all morning and can't figure out how to update my state correctly using useState.
I have a single controlled user input with name. When a user enters text and submits I would like to take the input value and push that to another state object namesList and map over that array in a child component.
Parent Component
import React, { Fragment, useState } from 'react';
import TextField from '#material-ui/core/TextField';
import NameInputList from './NameInputList';
const NameInputContainer = () => {
const [name, setName] = useState('');
const [namesList, setNamesList] = useState([]);
const handleChange = event => {
const { value } = event.target;
setName(value);
};
const handleSubmit = event => {
event.preventDefault();
setNamesList(prevState => [...prevState, name]);
setName('');
};
return (
<Fragment>
<form onSubmit={handleSubmit}>
<TextField
id="name"
label="Enter New Name"
variant="outlined"
value={name}
onChange={handleChange}
/>
</form>
{namesList.length > 0 && <NameInputList names={namesList} />}
</Fragment>
);
};
export default NameInputContainer;
Child Component
import React from 'react';
import PropTypes from 'prop-types';
const NameInputList = ({ names }) => {
console.log('child component names: ', names);
const generateKey = val => {
return `${val}_${new Date().getTime()}`;
};
return (
<ul>
{names.map((name, index) => ( // <--- Adding the index here seems to resolve the problem. I want to say the error was happening based on issues with having Unique keys.
<li key={generateKey(name + index)}>{name}</li>
))}
</ul>
);
};
NameInputList.propTypes = {
names: PropTypes.arrayOf(PropTypes.string)
};
NameInputList.defaultProps = {
names: []
};
export default NameInputList;
Seems like when I submit the first time the child component gets the correct value and renders as expected. When I go to input a new name there's a rerender on every handleChange. I'm not sure how to consistently: enter text > submit input > update namesList > render updated namesList in child component without handleChange breaking the functionality.
Just adding to that, as namesList prop is an array, a new copy will be sent for each parent re-render. Hence it's comparison will always be false and React will trigger a re-render of the child.
You can prevent the re-render by doing this :
export default React.memo(NameInputList, function(prevProps, nextProps) {
return prevProps.names.join("") === nextProps.names.join("");
});
This will ensure that NameInputList only re-renders when the contents of namesList actually change.