Sharing State Between Parent and Child Components in React: useImperativeHandle vs useShareState Hook
This article examines the common React modal communication problem, compares the conventional controlled‑component callback approach with the useImperativeHandle solution, and introduces a novel pair of hooks (useShareState and useShareValue) that enable seamless state sharing between parent and child components.
Preface
Recently I read a Juejin article titled “React officially does not recommend using useImperativeHandle, but I insist on using it, going against the official recommendation!” that explained how to expose component internal methods using ref and useImperativeHandle. This post, however, focuses on the underlying problem that the solution tries to solve – a common React component communication scenario.
The scenario is a modal component that must be opened by an external button and also be able to close itself after user interaction. A controlled modal cannot close itself, while an uncontrolled modal cannot be opened from outside.
Conventional Solutions
1. Controlled component with callbacks to parent
The parent keeps the open state, and the modal calls a callback when it wants to close.
<code style="padding: 16px; color: #333; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">Modal</span>() </span>{ ... }<br/><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">App</span> () </span>{<br/> <span style="font-weight: bold; line-height: 26px">const</span> [show, setShow] = useState(<span style="color: #008080; line-height: 26px">false</span>)<br/> <span style="font-weight: bold; line-height: 26px">const</span> onModalClose = <span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> setShow(<span style="color: #008080; line-height: 26px">false</span>)<br/> <span style="font-weight: bold; line-height: 26px">const</span> openModal = <span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> setShow(<span style="color: #008080; line-height: 26px">false</span>)<br/> <span style="font-weight: bold; line-height: 26px">return</span> (<br/> <><br/> <button click={openModal}> open <<span style="color: #009926; line-height: 26px">/button><br/> <Modal visible={show} onClose={onModalClose} /</span>><br/> <<span style="color: #009926; line-height: 26px">/><br/> )<br/>}<br/></code>2. Uncontrolled component exposing methods via useImperativeHandle
The modal manages its own visibility but exposes show and close methods through a ref.
<code style="padding: 16px; color: #333; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="font-weight: bold; line-height: 26px">const</span> Modal = forwardRef(<span style="line-height: 26px">(<span style="line-height: 26px">props, ref</span>) =></span> { <br/> <span style="font-weight: bold; line-height: 26px">const</span> [visible, setVisible] = useState(<span style="color: #008080; line-height: 26px">false</span>)<br/> <span style="font-weight: bold; line-height: 26px">const</span> show = <span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> setVisible(<span style="color: #008080; line-height: 26px">true</span>)<br/> <span style="font-weight: bold; line-height: 26px">const</span> close = <span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> setVisible(close)<br/> useImperativeHandle(ref, <span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> ({ show, close }))<br/> <span style="font-weight: bold; line-height: 26px">return</span> (<br/> <InnerModal visible={visible}><br/> <button onClick={close}>close by myself<<span style="color: #009926; line-height: 26px">/button><br/> </</span>InnerModal><br/> ); <br/>}, []);<br/><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">App</span> () </span>{<br/> <span style="font-weight: bold; line-height: 26px">const</span> modalRef = useRef(<span style="color: #008080; line-height: 26px">null</span>)<br/> <span style="font-weight: bold; line-height: 26px">return</span> (<br/> <><br/> <button click={modalRef.current?.show}> open <<span style="color: #009926; line-height: 26px">/button><br/> <Modal ref={modalRef} /</span>><br/> <<span style="color: #009926; line-height: 26px">/><br/> )<br/>}<br/></code>I prefer the second approach because it introduces less intrusion into the parent code.
However, using ref has a risk: if the component has not rendered yet, the exposed methods are unavailable, similar to Vue’s v-if or v-show issues.
I Discovered a “Magical” Solution
Inspired by a post about useControllableValue from ahooks, I experimented with a pair of hooks – useShareState and useShareValue – that allow a parent and child to share and update the same state.
Design Phase
Initially I imagined a simple usage where the parent passes a value to the child, and both can modify it.
<code style="padding: 16px; color: #333; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">Parent</span> () </span>{<br/> <span style="font-weight: bold; line-height: 26px">const</span> [state, setState] = useState(<span style="color: #008080; line-height: 26px">false</span>)<br/> <span style="font-weight: bold; line-height: 26px">return</span> ( <br/> <><br/> <button onClick={<span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> setState(<span style="color: #008080; line-height: 26px">true</span>)}> by parent <<span style="color: #009926; line-height: 26px">/button><br/> <Children value={state} /</span>><br/> <<span style="color: #009926; line-height: 26px">/><br/> )<br/>}<br/>function Children (props) {<br/> const { value } = props<br/> const [state, setState] = useState(value)<br/> return (<br/> <button onClick={() => setState(false)} > by myself </</span>button><br/> )<br/>}<br/></code>The idea is to merge the parent’s setState with the child’s setState so that calling either updates both.
Combining useShareState & useShareValue
useShareStatecreates a shared state object { _share: true, value, signalRecord }. useShareValue detects this shape, registers update signals, and ensures all components that share the state re‑render together.
<code style="padding: 16px; color: #333; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #998; font-style: italic; line-height: 26px">// use-share.ts</span><br/><span style="font-weight: bold; line-height: 26px">import</span> { useRef } <span style="font-weight: bold; line-height: 26px">from</span> <span style="color: #d14; line-height: 26px">"react"</span>;<br/><span style="font-weight: bold; line-height: 26px">import</span> { useUpdate, useUnmount } <span style="font-weight: bold; line-height: 26px">from</span> <span style="color: #d14; line-height: 26px">"ahooks"</span>;<br/><span style="font-weight: bold; line-height: 26px">type</span> Signal = ReturnType<<span style="font-weight: bold; line-height: 26px">typeof</span> useUpdate>;<br/><br/><span style="font-weight: bold; line-height: 26px">interface</span> ShareState<T = any> {<br/> _share: <span style="color: #008080; line-height: 26px">true</span>;<br/> value: T;<br/> signalRecord: Record<<span style="color: #0086b3; line-height: 26px">string</span>, Signal>;<br/>}<br/><br/><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">isShareState</span>(<span style="line-height: 26px">state: <span style="color: #0086b3; line-height: 26px">any</span></span>): <span style="color: #900; font-weight: bold; line-height: 26px">state</span> <span style="color: #900; font-weight: bold; line-height: 26px">is</span> <span style="color: #900; font-weight: bold; line-height: 26px">ShareState</span> </span>{<br/> <span style="font-weight: bold; line-height: 26px">return</span> state._share === <span style="color: #008080; line-height: 26px">true</span>;<br/>}<br/><br/><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">callSignals</span>(<span style="line-height: 26px">state: ShareState</span>) </span>{<br/> <span style="font-weight: bold; line-height: 26px">if</span> (state.signalRecord === <span style="color: #008080; line-height: 26px">null</span>) <span style="font-weight: bold; line-height: 26px">return</span>;<br/> <span style="color: #0086b3; line-height: 26px">Object</span>.values(state.signalRecord).forEach(<span style="line-height: 26px">(<span style="line-height: 26px">signal</span>) =></span> signal());<br/>}<br/><br/><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">createSeed</span>() </span>{<br/> <span style="font-weight: bold; line-height: 26px">return</span> <span style="color: #0086b3; line-height: 26px">String</span>(<span style="color: #0086b3; line-height: 26px">Math</span>.random());<br/>}<br/><br/><span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">disposeShareValue</span>(<span style="line-height: 26px">value: ShareState</span>) </span>{<br/> value.signalRecord = <span style="color: #0086b3; line-height: 26px">Object</span>.create(<span style="color: #008080; line-height: 26px">null</span>);<br/>}<br/><br/><span style="font-weight: bold; line-height: 26px">export</span> <span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">useShareState</span><<span style="color: #900; font-weight: bold; line-height: 26px">T</span>>(<span style="line-height: 26px">defaultValue: T</span>) </span>{<br/> <span style="font-weight: bold; line-height: 26px">const</span> update = useUpdate();<br/> <span style="font-weight: bold; line-height: 26px">const</span> seed = useRef(createSeed());<br/> <span style="font-weight: bold; line-height: 26px">const</span> value = useRef<ShareState<T>>({<br/> _share: <span style="color: #008080; line-height: 26px">true</span>,<br/> value: defaultValue,<br/> signalRecord: <span style="color: #0086b3; line-height: 26px">Object</span>.create(<span style="color: #008080; line-height: 26px">null</span>),<br/> });<br/><br/> Reflect.set(value.current.signalRecord, seed.current, update);<br/><br/> <span style="font-weight: bold; line-height: 26px">const</span> setState = <span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> (<span style="line-height: 26px">state: T</span>) </span>{<br/> value.current.value = state;<br/> callSignals(value.current);<br/> };<br/><br/> useUnmount(<span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> {<br/> disposeShareValue(value.current);<br/> });<br/><br/> <span style="font-weight: bold; line-height: 26px">return</span> [value.current, setState];<br/>}<br/><br/><span style="font-weight: bold; line-height: 26px">export</span> <span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">useShareValue</span><<span style="color: #900; font-weight: bold; line-height: 26px">T</span>>(<span style="line-height: 26px">value: T | ShareState<T></span>) </span>{<br/> <span style="font-weight: bold; line-height: 26px">const</span> seed = useRef(createSeed());<br/> <span style="font-weight: bold; line-height: 26px">const</span> update = useUpdate();<br/> <span style="font-weight: bold; line-height: 26px">const</span> isShare = isShareState(value);<br/><br/> useUnmount(<span style="line-height: 26px"><span style="line-height: 26px">()</span> =></span> {<br/> <span style="font-weight: bold; line-height: 26px">if</span> (isShare) {<br/> <span style="font-weight: bold; line-height: 26px">const</span> shareState = value;<br/> Reflect.deleteProperty(shareState.signalRecord, seed.current);<br/> }<br/> });<br/><br/> <span style="font-weight: bold; line-height: 26px">if</span> (isShare) {<br/> <span style="font-weight: bold; line-height: 26px">const</span> shareState = value;<br/> Reflect.set(shareState.signalRecord, seed.current, update);<br/><br/> <span style="line-height: 26px"><span style="font-weight: bold; line-height: 26px">function</span> <span style="color: #900; font-weight: bold; line-height: 26px">setState</span>(<span style="line-height: 26px">state: T</span>) </span>{<br/> shareState.value = state;<br/> callSignals(shareState);<br/> }<br/><br/> <span style="font-weight: bold; line-height: 26px">return</span> [shareState.value, setState, <span style="color: #008080; line-height: 26px">true</span>];<br/> }<br/><br/> <span style="font-weight: bold; line-height: 26px">return</span> [value, <span style="line-height: 26px">(<span style="line-height: 26px">...args: <span style="color: #0086b3; line-height: 26px">any</span>[]</span>) =></span> {}, <span style="color: #008080; line-height: 26px">false</span>];<br/>}<br/></code>Conclusion
The feature is still experimental and may need additional safeguards, such as supporting getter‑style initialization in useShareState. Feedback is welcome.
— Xekin
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
