React - change state from props (functional component)
In this article, we would like to show you how to change component state from props in React when we work with functional components.
This article shows two approaches:
- with
useEffect
what is the most popular approach in many projects that causes additional re-rendering component cycle, - with a custom hook that bases on
useRef
- which is the optimal solution.
1. useEffect
approach example
Below example uses two functional components: MyComponent
and App
.
States are stored in globalCounter
and localCounter
. Every button
click action inside App
component causes passing globalCounter
into MyComponent
localCounter
state via counter
props. useState
creates state with indicated value only once, so in the below case useState(counter)
with new couter
value will not cause state change. By using useEffect
hook we are able to monitor counter
prop value with indicating dependencies via [counter]
and update localCounter
value with setLocalCounter
function always when useEffect
detects couter
value changes.
// ONLINE-RUNNER:browser;
//Note: Uncomment import lines during working with JSX Compiler.
// import React from 'react';
// import ReactDOM from 'react-dom';
const MyComponent = ({counter}) => {
const [localCouter, setLocalCouter] = React.useState(counter);
React.useEffect(() => {
setLocalCouter(counter);
}, [counter]); // <--- when counter value is changed setLocalCouter updates local state
return (
<div>
<span>localCouter={localCouter}</span>
<button onClick={() => setLocalCouter(localCouter + 1)}>Click me!</button>
</div>
);
};
const App = () => {
const [globalCounter, setGlobalCouter] = React.useState(0);
return (
<div>
<span>globalCounter={globalCounter}</span>
<button onClick={() => setGlobalCouter(globalCounter + 1)}>Click me!</button>
<MyComponent counter={globalCounter} />
</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);
2. Optimal solution
Below examples uses useRef
and useState
to store property and state.
2.1. with useState
// ONLINE-RUNNER:browser;
//Note: Uncomment import lines during working with JSX Compiler.
// import React from 'react';
// import ReactDOM from 'react-dom';
// This approach reduces unnecessary re-rendering.
const usePropState = (prop) => {
const [current, setCurrent] = React.useState(() => ({prop, state: prop}));
if (current.prop != prop) {
current.prop = prop;
current.state = prop;
}
return [
current.state,
(state) => {
setCurrent({prop, state});
}
];
};
const MyComponent = ({ counter }) => {
const [localCouter, setLocalCouter] = usePropState(counter);
return (
<div>
<span>localCouter={localCouter}</span>
<button onClick={() => setLocalCouter(localCouter + 1)}>Click me!</button>
</div>
);
};
const App = () => {
const [globalCounter, setGlobalCouter] = React.useState(0);
return (
<div>
<span>globalCounter={globalCounter}</span>
<button onClick={() => setGlobalCouter(globalCounter + 1)}>Click me!</button>
<MyComponent counter={globalCounter} />
</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);
2.2. with useRef
example
Below approach is a little complicated because uses references to store state. The main idea of the solution is to store state inside reference and force rerendering with counter
when the state is changed.
// ONLINE-RUNNER:browser;
//Note: Uncomment import lines during working with JSX Compiler.
// import React from 'react';
// import ReactDOM from 'react-dom';
// This approach reduces unnecessary re-rendering.
const usePropState = (prop) => {
const [counter, setCounter] = React.useState(0); // to force re-rendering only
const currentPropRef = React.useRef(prop); // to help in detection property change only
const currentStateRef = React.useRef(prop); // stores local state
if (currentPropRef.current != prop) { // property change detection
currentPropRef.current = prop;
currentStateRef.current = prop;
}
return [
currentStateRef.current,
(newState) => {
currentStateRef.current = newState;
setCounter(counter => counter + 1);
}
];
};
const MyComponent = ({ counter }) => {
const [localCouter, setLocalCouter] = usePropState(counter);
return (
<div>
<span>localCouter={localCouter}</span>
<button onClick={() => setLocalCouter(localCouter + 1)}>Click me!</button>
</div>
);
};
const App = () => {
const [globalCounter, setGlobalCouter] = React.useState(0);
return (
<div>
<span>globalCounter={globalCounter}</span>
<button onClick={() => setGlobalCouter(globalCounter + 1)}>Click me!</button>
<MyComponent counter={globalCounter} />
</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);