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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Sharing State Between Parent and Child Components in React: useImperativeHandle vs useShareState Hook

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

useShareState

creates 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendReactstate sharingCustom HooksuseImperativeHandle
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.