When researching contexts I've stumbled across a common pattern more than once:
const storeContext = React.createContext()
const Provider = (children) => {
const [state, setState] = React.useState();
const ctxValue = React.useMemo(() => [state, setState], [state]);
return (
<storeContext.Provider value={ctxValue}>
{children}
</storeContext.Provider>
)
}
Many people seem to use useMemoin this case, and I'm not sure why. At first glance, it seems to make sense since passing an object/array directly would mean that this object would get recreated on every render and trigger the children to be rerendered since a new reference is passed. At second glance, I struggle with the pattern.
I'm not sure how this pattern could potentially prevent any rerenders. If a component up the tree changes, all children (including the context) rerender anyway, and if state changes, the provider rerenders (rightfully) itself as well as all children.
The only thing that I see useMemo do is to save a tiny bit of memory since we're not recreating the ctxValue object on every render.
I'm not sure if I'm missing something here, and would love some input.
The reason is because every render would create a new Array instance. By using useMemo, the array passed to the context is only ever changed when state changes. A better way to do this would be to just pass the result of useState to the context:
const storeContext = React.createContext()
const Provider = (children) => {
const ctxValue = React.useState();
return (
<storeContext.Provider value={ctxValue}>
{children}
</storeContext.Provider>
)
}
Update
So, I was pretty curious as to how React would handle re-renders and contexts updating. I was really surprised to find that React will only re-render a component if the props change.
For example, if you have something like this:
function MyComponent(){
const ctxValue = useContext(MyContext);
return (<div>
value: {ctxValue}
<MyOtherComponent />
</div>);
}
And you render that component, MyOtherComponent will only render once, ever, assuming the component doesn't use any hooks that can update its internal state.
The same goes for Context.Provider. The only time it will re-render is when the value updates, but its children will not re-render unless they directly depend on that context.
I create a simple sandbox to demonstrate this.
Basically, I set up a lot of different ways to nest children, and some children rely on the context, and others don't. If you click the "+" button, the context is updated and you'll see the render count only increase for those components.
const Ctx = React.createContext();
function useRenderCount(){
const count = React.useRef(0);
count.current += 1;
return count.current;
}
function wrapInRenderCount(name,child){
const count = useRenderCount();
return (
<div>
<div>
{name} was rendered {count} times.
</div>
<div style={{ marginLeft: "1em" }}>{child}</div>
</div>
);
}
function ContextProvider(props) {
const [count, setState] = React.useState(0);
const increase = React.useCallback(() => {
setState((v) => v + 1);
}, []);
const context = React.useMemo(() => [increase, count], [increase, count]);
return wrapInRenderCount('ContextProvider',
<Ctx.Provider value={context}>
{props.children}
</Ctx.Provider>
);
}
function ContextUser(props){
const [increase, count] = React.useContext(Ctx);
return wrapInRenderCount('ContextUser',
<div>
<div>
Count: {count}
<button onClick={increase}>
++
</button>
</div>
<div>
{props.children}
</div>
</div>
);
}
function RandomWrapper(props){
return wrapInRenderCount('RandomWrapper',<div>
<div>
Wrapped
</div>
<div style={{marginLeft:'1em'}}>
{props.children}
</div>
</div>);
}
function RandomContextUserWrapper(props){
return wrapInRenderCount('RandomContextUserWrapper',
<div>
<ContextUser></ContextUser>
{props.children}
</div>
)
}
function App() {
return wrapInRenderCount('App',
<RandomWrapper>
<ContextProvider>
<RandomWrapper>
<ContextUser>
<RandomWrapper>
<RandomContextUserWrapper>
<RandomWrapper>
<ContextUser/>
</RandomWrapper>
</RandomContextUserWrapper>
</RandomWrapper>
</ContextUser>
<RandomWrapper>
<RandomContextUserWrapper />
</RandomWrapper>
</RandomWrapper>
</ContextProvider>
</RandomWrapper>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<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="root"></div>
Related
I have the following React code
const { useState, useMemo, Fragment } = React;
function Rand() {
return <Fragment>{Math.random()}</Fragment>;
}
const App = () => {
const [show, setShow] = useState(true);
// The inline component gets memoized. But <Rand /> does not
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
// The rand component is not memoized and gets rerendred
const notWorking = useMemo(() => <Rand />, []);
return(
<Fragment>
<button
onClick={() => {
setShow(!show);
}}>
{show?"Hide":"Show"}
</button>
<br />
Working:
{show && working}
<br />
Not Working:
{show && notWorking}
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
It uses useMemo 2 times.
The first time it uses an inline component to "initialize" and memoize a component ( const working = useMemo(() => <>{Math.random()}</>, []);)
The second time it uses a component which was made outside the app component (const notWorking = useMemo(() => <Rand />, []);)
Both components used in the useMemo function have the exact same code, which is <>{Math.random()}</>.
Here comes the unexpected part, when I hide (Click the button) and show the two memoized components again, they behave differently. The first one will always show the same random number which it got when it first got initialzied. While the seconds one will re-initialize each time.
First render
Second render (hide)
Third render (show again)
You can see from the screenshots that the first component's random number stays the same, while the second one does not.
My Questions:
How can I prevent in both cases to re-render/re-initialize the component?
Why does it currently behave as it does?
Interestingly, it does get memoized if I use a counter instead of show/hide:
const { useState, useMemo, Fragment } = React;
function Rand() {
return <Fragment>{Math.random()}</Fragment>;
}
const App = () => {
const [counter, setCounter] = useState(0);
// The inline component gets memoized. But <Rand /> does not
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
// The rand component is not memoized and gets rerendred
const notWorking = useMemo(() => <Rand />, []);
return(
<Fragment>
<button
onClick={() => {
setCounter(c => c + 1);
}}>
Update ({counter})
</button>
<br />
Working:
{working}
<br />
Not Working:
{notWorking}
<br />
<code>Rand</code> used directly:
<Rand />
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Here is a codepen to try it yourself https://codepen.io/brandiatmuhkuh/pen/eYWmyWz
Why does it currently behave as it does?
<Rand /> doesn't call your component function. It just calls React.createElement to create the React element (not an instance of it). Your component function is used to render an element instance, if and when you use it. In your "working" example you're doing:
<>{Math.random()}</>
...which calls Math.random and uses its result as text (not a component) within the fragment.
But your "not working" example just does:
<Rand />
The element is created, but not used, and your function isn't called. The "your function isn't called" part may be surprising — it was to me when I started using React — but it's true:
const { Fragment } = React;
function Rand() {
console.log("Rand called");
return <Fragment>{Math.random()}</Fragment>;
}
console.log("before");
const element = <Rand />;
console.log("after");
// Wait a moment before *using* it
setTimeout(() => {
ReactDOM.render(
element,
document.getElementById("root")
);
}, 1000);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
How can I prevent in both cases to re-render/re-initialize the component?
If you do what you've done in your example, which is to take the mounted component out of the tree entirely, you're unmounting the component instance; when you put it back, your function will get called again, so you'll get a new value. (This is also why the version with the counter doesn't exhibit this behavior: the component instance remained mounted.)
If you want to memoize what it shows, one approach is to pass that to it as a prop, and memoize what you pass it:
const { useState, useMemo, Fragment } = React;
function Rand({text}) {
return <Fragment>{text}</Fragment>;
}
const App = () => {
const [show, setShow] = useState(true);
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
const randText = useMemo(() => String(Math.random()), []);
return(
<Fragment>
<button
onClick={() => {
setShow(!show);
}}>
{show?"Hide":"Show"}
</button>
<br />
Working:
{show && working}
<br />
Also Working Now:
{show && <Rand text={randText} />}
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
I am developing a basic quiz app in react. I have a QuestionList component which sends question,options to another component Question as props. Inside the Question component while trying the following code
<div>
{props.question}
</div>
<div>
{props.options.map(option=>{return(
<div> <button onClick={()=> submitAnswer(option.isCorrect)} >{option.option}</button> </div>)
})}
</div>
It is giving me an error that Cannot read property map of unknown.
But when I remove the second div, the question is rendered and after that if I paste the second div and save it the options are also rendered.
If I refresh the page in browser the error is getting repeated. Why is this weird behavior
EDIT
options is an array of objects
options:[
{option:"Hyderabad",isCorrect:false},
{ option:"Delhi",isCorrect:true},
]
Parent component
import React, { useEffect, useState } from 'react';
import Question from "./Question"
const QuestionList=()=>{
const [questions,setQuestions]=useState([{}]);
useEffect(()=>{
async function fetchQuestions(){
const response=await fetch("http://localhost:7000/questions");
const data=await response.json();
setQuestions(data.questions);
console.log(data.questions)
}
fetchQuestions();
console.log(questions)
},[])// eslint-disable-line react-hooks/exhaustive-deps
const [currentQuestion,setCurrentQuestion]=useState(0);
const [score,setScore]=useState(0);
const [showScore,setShowScore]=useState(false);
const handleOptionCorrect=(isCorrect)=>{
if(isCorrect)
{
setScore(score+1)
}
const nextQuestion=currentQuestion+1;
if(nextQuestion<questions.length){
setCurrentQuestion(nextQuestion)
}
else{
setShowScore(true)
}
}
return(
<div>
{showScore?(
<div>
Your score is {score} / {questions.length}
</div>
):(
<>
<Question question={questions[currentQuestion].question}
options={questions[currentQuestion].options}
handleOptionCorrect={handleOptionCorrect}/>
</>
)}
</div>
);
}
export default QuestionList;
Child Component
import React from 'react'
const Question=(props)=>{
const submitAnswer=(isCorrect)=>{
props.handleOptionCorrect(isCorrect)
}// eslint-disable-next-line
return(
<>
<div>
{props.question}
<div>
{props.options.map(option=>{return(
<div> <button onClick={()=> submitAnswer(option.isCorrect)} >{option.option}</button> </div>)
})}
</div>
</div>
</>
);
}
export default Question
Resolved
Previously I was taking options directly from props. After looking at #Naren 's answer I got an idea how to take options array from props and edited my code accordingly and it worked.
Thank you!
Maybe QuestionList receiving props(question, options) as undefined. Better to add fallbacks. Please add more code to your question to get better understanding.
const QuestionList = (props) => {
const { question, options = [] } = props
if (!question) return <span>loading question...</span>
....
}
Issue
Your initial questions state is an array with an empty object but your are selecting the the first element and sending an undefined options value.
const [questions, setQuestions] = useState([{}]);
const [currentQuestion, setCurrentQuestion] = useState(0);
<Question
question={questions[currentQuestion].question}
options={questions[currentQuestion].options} // <-- options is undefined
handleOptionCorrect={handleOptionCorrect}
/>
Child - throws error because props.options is undefined.
{props.options.map(option=>{return(
<div>
<button onClick={()=> submitAnswer(option.isCorrect)}>
{option.option}
</button>
</div>)
})}
Solution
You should provide valid initial state that doesn't blow up your UI.
const [questions, setQuestions] = useState([{ options: [] }]); // <-- provide options array
const [currentQuestion, setCurrentQuestion] = useState(0);
<Question
question={questions[currentQuestion].question}
options={questions[currentQuestion].options} // <-- options is defined
handleOptionCorrect={handleOptionCorrect}
/>
Suggestion
If Question component is rendered by other components then it's good to provide a fallback value in case you forget to always provide valid props.
const Question = ({ handleOptionCorrect, options = [], question })=>{
const submitAnswer=(isCorrect)=>{
handleOptionCorrect(isCorrect)
}
return (
<>
<div>
{question}
<div>
{options.map(option=> (
<div>
<button onClick={()=> submitAnswer(option.isCorrect)} >
{option.option}
</button>
</div>)
)}
</>
);
}
My main functional component performs a huge amount of useQueries and useMutations on the child component hence I have set it as React.memo so as to not cause re-rendering on each update. Basically, when new products are selected I still see the old products because of memo.
mainFunction.js
const [active, setActive] = useState(false);
const handleToggle = () => setActive(false);
const handleSelection = (resources) => {
const idsFromResources = resources.selection.map((product) => product.variants.map(variant => variant.id));
store.set('bulk-ids', idsFromResources); //loal storage js-store library
handleToggle
};
const emptyState = !store.get('bulk-ids'); // Checks local storage using js-store library
return (
<Page>
<TitleBar
title="Products"
primaryAction={{
content: 'Select products',
onAction: () => {
setActive(!active)
}
}}
/>
<ResourcePicker
resourceType="Product"
showVariants={true}
open={active}
onSelection={(resources) => handleSelection(resources)}
onCancel={handleToggle}
/>
<Button >Add Discount to Products</Button> //Apollo useMutation
{emptyState ? (
<Layout>
Select products to continue
</Layout>
) : (
<ChildComponent />
)}
</Page>
);
ChildComponent.js
class ChildComponent extends React {
return(
store.get(bulk-ids).map((product)=>{
<Query query={GET_VARIANTS} variables={{ id: variant }}>
{({ data, extensions, loading, error }) => {
<Layout>
// Query result UI
<Layout>
}}
</Query>
})
)
}
export deafult React.memo(ChildComponent);
React.memo() is useful when your component always renders the same way with no changes. In your case you need to re-render <ChildComponent> every time bulk-id changes. So you should use useMemo() hook.
function parentComponent() {
... rest of code
const bulkIds = store.get('bulk-ids');
const childComponentMemo = useMemo(() => <ChildComponent ids={bulkIds}/>, [bulkIds]);
return <Page>
... rest of render
{bulkIds ?
childComponentMemo
:(
<Layout>
Select products to continue
</Layout>
)}
</Page>
}
useMemo() returns the same value until buldIds has not changed. More details about useMemo() you can find here.
I'm using functional components and React-Redux to manage state. Simply speaking, I hope that even if the state changes, rendering will not happen. First of all, the code is as follows. It is a simple code for explanation.
const NOUPDATE = ({ uploadAnswer, noState }) => (
<>
<div className="content">
<div className="container">
<p className="directions" />
<ul className="qbox">{noState ? (<p>true</p>) : (<p>false</p>)}</ul>
<button onClick={uploadAnswer}>upload</button>
</div>
</div>
</>
);
const mapStateToProps = state => ({
noState: state.problemInfoReducer.checkMode,
});
const mapDispatchToProps = dispatch => ({
uploadAnswer: value => dispatch(uploadAnswer(value)),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(NOUPDATE);
uploadAnswer reverses noState each time it is dispatched. So if I dispatch uploadAnswer every time, noState is updated, and the screen is re-rendered as a whole. I don't want the screen to be re-rendered even if noState is updated.
Here is a solution that allows you to keep the qbox boolean toggle without rendering the parent component. Essesntially, you use Redux to your advantage by creating a child component that's mapped to the noState property rather than having that mapping in the parent component.
NOUPDATE.jsx
import Qbox from "./Qbox";
const NOUPDATE = ({ uploadAnswer }) => (
<>
<div className="content">
<div className="container">
<p className="directions" />
<Qbox />
<button onClick={uploadAnswer}>upload</button>
</div>
</div>
</>
);
const mapDispatchToProps = dispatch => ({
uploadAnswer: value => dispatch(uploadAnswer(value)),
});
export default connect(
null,
mapDispatchToProps,
)(NOUPDATE);
Qbox.jsx
const Qbox = ({noState}) => <ul className="qbox">{noState ? (<p>true</p>) : (<p>false</p>)}</ul>;
const mapStateToProps = state => ({
noState: state.problemInfoReducer.checkMode,
});
export default connect(
mapStateToProps
)(Qbox);
You can use React.memo to memoize your component. The second argument passed to React.memo is a function that takes oldProps and newProps. Return false when the component should be updated.
Below, I return false only when the uploadAnswer doesn't match between oldProps and newProps, so that's the only time the component will re-render.
const NOUPDATE = React.memo(
({ uploadAnswer, noState }) => (
<>
<div className="content">
<div className="container">
<p className="directions" />
<ul className="qbox">{noState ? <p>true</p> : <p>false</p>}</ul>
<button onClick={uploadAnswer}>upload</button>
</div>
</div>
</>
),
(oldProps, newProps) => {
if (oldProps.uploadAnswer !== newProps.uploadAnswer) {
return false;
}
return true;
}
);
I'm looking for the easiest solution to pass data from a child component to his parent.
I've heard about using Context, pass trough properties or update props, but I don't know which one is the best solution.
I'm building an admin interface, with a PageComponent that contains a ChildComponent with a table where I can select multiple line. I want to send to my parent PageComponent the number of line I've selected in my ChildComponent.
Something like that :
PageComponent :
<div className="App">
<EnhancedTable />
<h2>count 0</h2>
(count should be updated from child)
</div>
ChildComponent :
const EnhancedTable = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me {count}
</button>
)
};
I'm sure it's a pretty simple thing to do, I don't want to use redux for that.
A common technique for these situations is to lift the state up to the first common ancestor of all the components that needs to use the state (i.e. the PageComponent in this case) and pass down the state and state-altering functions to the child components as props.
Example
const { useState } = React;
function PageComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1)
}
return (
<div className="App">
<ChildComponent onClick={increment} count={count} />
<h2>count {count}</h2>
(count should be updated from child)
</div>
);
}
const ChildComponent = ({ onClick, count }) => {
return (
<button onClick={onClick}>
Click me {count}
</button>
)
};
ReactDOM.render(<PageComponent />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You can create a method in your parent component, pass it to child component and call it from props every time child's state changes, keeping the state in child component.
const EnhancedTable = ({ parentCallback }) => {
const [count, setCount] = useState(0);
return (
<button onClick={() => {
const newValue = count + 1;
setCount(newValue);
parentCallback(newValue);
}}>
Click me {count}
</button>
)
};
class PageComponent extends React.Component {
callback = (count) => {
// do something with value in parent component, like save to state
}
render() {
return (
<div className="App">
<EnhancedTable parentCallback={this.callback} />
<h2>count 0</h2>
(count should be updated from child)
</div>
)
}
}
To make things super simple you can actually share state setters to children and now they have the access to set the state of its parent.
example:
Assume there are 4 components as below,
function App() {
return (
<div className="App">
<GrandParent />
</div>
);
}
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
return (
<>
<div>{name}</div>
<Parent setName={setName} />
</>
);
};
const Parent = params => {
return (
<>
<button onClick={() => params.setName("i'm from Parent")}>
from Parent
</button>
<Child setName={params.setName} />
</>
);
};
const Child = params => {
return (
<>
<button onClick={() => params.setName("i'm from Child")}>
from Child
</button>
</>
);
};
so grandparent component has the actual state and by sharing the setter method (setName) to parent and child, they get the access to change the state of the grandparent.
you can find the working code in below sandbox,
https://codesandbox.io/embed/async-fire-kl197
IF we Have Parent Class Component and Child function component this is how we going to access child component useStates hooks value :--
class parent extends Component() {
constructor(props){
super(props)
this.ChildComponentRef = React.createRef()
}
render(){
console.log(' check child stateValue: ',
this.ChildComponentRef.current.info);
return (<> <ChildComponent ref={this.ChildComponentRef} /> </>)
}
}
Child Component we would create using
React.forwardRef((props, ref) => (<></>))
. and
useImperativeHandle(ref, createHandle, [deps])
to customizes the instance value that is exposed to parent components
const childComponent = React.forwardRef((props, ref) => {
const [info, setInfo] = useState("")
useEffect(() => {
axios.get("someUrl").then((data)=>setInfo(data))
})
useImperativeHandle(ref, () => {
return {
info: info
}
})
return (<> <h2> Child Component <h2> </>)
})
I had to do this in type script. The object-oriented aspect would need the dev to add this callback method as a field in the interface after inheriting from parent and the type of this prop would be Function. I found this cool!
Here's an another example of how we can pass state directly to the parent.
I modified a component example from react-select library which is a CreatableSelect component. The component was originally developed as class based component, I turned it into a functional component and changed state manipulation algorithm.
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
console.group('Value Changed');
console.log(value);
console.log(`action: ${actionMeta.action}`);
console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
console.group('Value Added');
console.log(props.value);
console.groupEnd();
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue)])
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type something and press enter..."
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I call it from the pages of my next js project like this
import MultiSelectTextInput from "../components/Form/MultiSelect/MultiSelectTextInput";
const NcciLite = () => {
const [value, setValue] = useState<any>([]);
const [inputValue, setInputValue] = useState<any>('');
return (
<React.Fragment>
....
<div className="d-inline-flex col-md-9">
<MultiSelectTextInput
id="codes"
value={value}
setValue={setValue}
inputValue={inputValue}
setInputValue={setInputValue}
/>
</div>
...
</React.Fragment>
);
};
As seen, the component modifies the page's (parent page's) state in which it is called.
I've had to deal with a similar issue, and found another approach, using an object to reference the states between different functions, and in the same file.
import React, { useState } from "react";
let myState = {};
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
myState.name=name;
myState.setName=setName;
return (
<>
<div>{name}</div>
<Parent />
</>
);
};
export default GrandParent;
const Parent = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Parent")}>
from Parent
</button>
<Child />
</>
);
};
const Child = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Child")}>
from Child
</button>
</>
);
};