Child to Parent Communication in ReactJS: 6 Proven Patterns from the Source Code

The most effective methods for child-to-parent communication in ReactJS include callback props for simple events, the Context API for deep component trees, and useImperativeHandle with forwardRef for imperative actions, all implemented through core hooks defined in the facebook/react repository.

React’s architecture enforces unidirectional data flow where props travel downward from parent to child. When a child component needs to communicate upward, developers must use specific patterns that preserve this immutable, declarative model while maintaining type safety and performance. The facebook/react repository provides several first-class mechanisms—implemented in packages/react/src/ReactHooks.js and related core files—that enable this communication without breaking the component encapsulation model.

1. Callback Props for Event-Driven Communication

The most fundamental pattern involves the parent passing a function to the child via props. The child invokes this function with data, effectively signaling upward intent without mutating parent state directly.

According to the source code in packages/react/src/ReactHooks.js (lines 35-41), parents should wrap these callbacks with useCallback to maintain reference equality across renders. Without this optimization, inline function declarations create new references on every render, causing unnecessary re-renders of memoized child components.

// Parent.jsx
import {useState, useCallback} from 'react';
import Child from './Child';

export default function Parent() {
  const [message, setMessage] = useState('');

  const handleUpdate = useCallback(newMsg => {
    setMessage(newMsg);
  }, []);

  return (
    <>
      <Child onUpdate={handleUpdate} />
      <p>Message from child: {message}</p>
    </>
  );
}
// Child.jsx
export default function Child({onUpdate}) {
  return (
    <button onClick={() => onUpdate('Hello from child!')}>
      Send to Parent
    </button>
  );
}

2. Lifting State Up to Common Ancestors

When multiple siblings need to share data or the parent must derive UI from child input, state should reside in the nearest common ancestor. Both the parent and child receive the current value via props, and the child updates it through callback props.

This pattern relies on useState, implemented in packages/react/src/ReactHooks.js (lines 1-22), which provides the setter function that eventually reaches the child as a callback. The parent controls what the child can do by exposing specific setter functions rather than the raw state.

3. Imperative Handles with forwardRef and useImperativeHandle

For cases where the parent must trigger functions not naturally expressed as data flow—such as focusing an input, scrolling to a position, or playing media—React provides the forwardRef API coupled with useImperativeHandle.

The forwardRef implementation in packages/react/src/ReactForwardRef.js (lines 12-55) validates the render function at development time (lines 18-30), ensuring the component accepts the ref argument correctly. Meanwhile, useImperativeHandle in packages/react/src/ReactHooks.js (lines 51-57) warns if the ref argument is not an object or function, providing runtime safety.

// FancyInput.jsx
import React, {forwardRef, useImperativeHandle, useRef} from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // expose a `focus` method to the parent
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current && inputRef.current.focus(),
    clear: () => (inputRef.current.value = ''),
  }));

  return <input ref={inputRef} {...props} />;
});

export default FancyInput;
// Parent.jsx
import React, {useRef} from 'react';
import FancyInput from './FancyInput';

export default function Parent() {
  const fancyRef = useRef();

  return (
    <>
      <FancyInput ref={fancyRef} placeholder="Click button to focus" />
      <button onClick={() => fancyRef.current.focus()}>
        Focus Input
      </button>
      <button onClick={() => fancyRef.current.clear()}>
        Clear Input
      </button>
    </>
  );
}

4. Context API for Deep Component Trees

Passing callbacks through multiple layers of components creates prop drilling. The Context API, defined in packages/react/src/ReactContext.js (lines 14-23), creates a store that any descendant can access via useContext.

The parent supplies the value through a Provider, exposing both data and setter functions to deeply nested children without intermediate components forwarding props. This turns the parent into a provider of shared data while maintaining React's declarative nature.

// MessageContext.js
import React, {createContext, useState} from 'react';

export const MessageContext = createContext({
  message: '',
  setMessage: () => {},
});

export function MessageProvider({children}) {
  const [message, setMessage] = useState('');
  return (
    <MessageContext.Provider value={{message, setMessage}}>
      {children}
    </MessageContext.Provider>
  );
}
// DeepChild.jsx
import React, {useContext} from 'react';
import {MessageContext} from './MessageContext';

export default function DeepChild() {
  const {setMessage} = useContext(MessageContext);
  return (
    <button onClick={() => setMessage('Sent from deep child')}>
      Update Parent (via Context)
    </button>
  );
}
// App.jsx
import {MessageProvider} from './MessageContext';
import DeepChild from './DeepChild';
import {useContext} from 'react';
import {MessageContext} from './MessageContext';

function Display() {
  const {message} = useContext(MessageContext);
  return <p>Current message: {message}</p>;
}

export default function App() {
  return (
    <MessageProvider>
      <Display />
      <DeepChild />
    </MessageProvider>
  );
}

5. useReducer and Dispatch Functions

For complex state transitions, the parent creates a reducer and passes the dispatch function to children. Children communicate by dispatching actions ({type: 'INCREMENT', payload}) rather than calling callbacks directly, keeping the data flow explicit and testable.

The useReducer hook shares the same core implementation as useState in packages/react/src/ReactHooks.js (lines 1-22), ensuring consistent scheduling and batching of updates.

// Parent.jsx
import React, {useReducer} from 'react';
import Child from './Child';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state, count: state.count + 1};
    default:
      return state;
  }
}

export default function Parent() {
  const [state, dispatch] = useReducer(reducer, {count: 0});

  return (
    <>
      <Child dispatch={dispatch} />
      <p>Count: {state.count}</p>
    </>
  );
}
// Child.jsx
export default function Child({dispatch}) {
  return (
    <button onClick={() => dispatch({type: 'increment'})}>
      Increment in Parent
    </button>
  );
}

6. useSubscription for External Stores

When integrating with third-party state containers or observables outside React's tree, the useSubscription hook provides a standardized way to subscribe to external data sources and receive updates. This pattern is implemented in packages/use-subscription/src/useSubscription.js and is commonly used by state-container libraries like Redux to notify React components of external changes.

Performance and Safety Considerations

To optimize child-to-parent communication according to the React source architecture:

  • Wrap callbacks with useCallback and derived data with useMemo to preserve reference equality. The implementation in ReactHooks.js ensures these hooks maintain stable references across renders when dependency arrays match.
  • Memoize imperative handles. When using useImperativeHandle, memoize the returned object to avoid creating new method instances each render.
  • Keep Context values granular. Avoid placing large mutable objects in the Provider value to prevent widespread re-renders of all consumers.

Type-checking is enforced at the component boundary. forwardRef validates render functions during development (as seen in ReactForwardRef.js lines 18-30), and useImperativeHandle validates ref argument types (lines 51-55 of ReactHooks.js), providing development-time warnings before runtime errors occur.

Real-world applications often combine these patterns. For example, a form component might lift state up to share data with siblings while also exposing an imperative handle via useImperativeHandle to allow the parent to reset the form programmatically.

Summary

Frequently Asked Questions

What is the difference between callback props and useImperativeHandle?

Callback props facilitate declarative communication where the child notifies the parent of events or data changes through function invocation. useImperativeHandle provides an imperative escape hatch where the parent directly invokes methods on the child, useful for focus management, media controls, or animations that don't fit the declarative data-flow model.

When should I use Context API instead of callback props?

Use Context API when components are separated by multiple intermediate layers, making prop drilling cumbersome and error-prone. According to the ReactContext.js implementation, Context is most efficient for truly global or deeply shared state. For direct parent-child relationships, callback props remain preferable to maintain component encapsulation and explicit data contracts.

How does useCallback prevent performance issues in child-to-parent communication?

According to the implementation in packages/react/src/ReactHooks.js (lines 35-41), useCallback memoizes the function reference across renders. Without it, inline arrow functions create new references on every parent render, causing React to re-render pure children unnecessarily. This preservation of reference equality ensures that React.memo or PureComponent optimizations in children remain effective.

Is it safe to pass the dispatch function from useReducer to deep child components?

Yes. The dispatch function from useReducer maintains a stable reference across renders and does not cause re-renders of consuming components unless they also subscribe to the state. This makes it safe to pass through Context or deep prop chains without memoization concerns, unlike typical callback functions that require useCallback wrapping.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →