Frontend Development 16 min read

Mastering React Component Communication: 7 Essential Techniques

Explore seven core methods for React component communication—including Props, State Lifting, Context, Refs, Hooks, Event Emitters, and Redux—detailing practical code examples, key considerations, and best practices to help developers choose the optimal approach for various application scenarios.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Mastering React Component Communication: 7 Essential Techniques

Today we discuss React component communication methods.

1. Props: Basic communication

In React, props are the primary way for a parent component to pass data to a child component.

Example

<code>// ParentComponent.jsx
import React from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  const message = "Hello from Parent";

  return (
    <div>
      <ChildComponent message={message} />
    </div>
  );
};

export default ParentComponent;

// ChildComponent.jsx
import React from 'react';

const ChildComponent = ({ message }) => {
  return (
    <div>
      <p>{message}</p>
    </div>
  );
};

export default ChildComponent;</code>

Analysis

In this example, ParentComponent passes a string message to ChildComponent via props, and the child displays it inside a &lt;p&gt; element.

Notes

Props are read‑only; child components cannot modify them.

Props can be any JavaScript value, including objects and functions.

2. State Lifting: Share state via a common parent

When multiple components need the same state, lift the state to their nearest common ancestor and pass it down as props.

Example

<code>// ParentComponent.jsx
import React, { useState } from 'react';
import ChildComponentA from './ChildComponentA';
import ChildComponentB from './ChildComponentB';

const ParentComponent = () => {
  const [sharedState, setSharedState] = useState("Initial State");

  const handleChange = (newState) => {
    setSharedState(newState);
  };

  return (
    <div>
      <ChildComponentA sharedState={sharedState} onChange={handleChange} />
      <ChildComponentB sharedState={sharedState} />
    </div>
  );
};

export default ParentComponent;

// ChildComponentA.jsx
import React from 'react';

const ChildComponentA = ({ sharedState, onChange }) => {
  return (
    <div>
      <input
        type="text"
        value={sharedState}
        onChange={(e) => onChange(e.target.value)}
      />
    </div>
  );
};

export default ChildComponentA;

// ChildComponentB.jsx
import React from 'react';

const ChildComponentB = ({ sharedState }) => {
  return (
    <div>
      <p>{sharedState}</p>
    </div>
  );
};

export default ChildComponentB;</code>

Analysis

The ParentComponent holds sharedState and provides a handleChange function. ChildComponentA can update the state, while ChildComponentB only displays it.

Notes

Lifting state can make the parent component more complex.

Only lift state when necessary to avoid over‑complicating the component hierarchy.

3. Context: Avoid prop‑drilling

Context lets you share data across many levels of the component tree without passing props through every intermediate component.

Example

<code>// MyContext.js
import React from 'react';
const MyContext = React.createContext();
export default MyContext;

// ParentComponent.jsx
import React, { useState } from 'react';
import MyContext from './MyContext';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  const [sharedState, setSharedState] = useState("Initial State");

  return (
    <MyContext.Provider value={{ sharedState, setSharedState }}>
      <ChildComponent />
    </MyContext.Provider>
  );
};

export default ParentComponent;

// ChildComponent.jsx
import React, { useContext } from 'react';
import MyContext from './MyContext';

const ChildComponent = () => {
  const { sharedState, setSharedState } = useContext(MyContext);

  return (
    <div>
      <input
        type="text"
        value={sharedState}
        onChange={(e) => setSharedState(e.target.value)}
      />
      <p>{sharedState}</p>
    </div>
  );
};

export default ChildComponent;</code>

Analysis

MyContext is created and provided by ParentComponent . ChildComponent consumes the context with useContext to read and update sharedState .

Notes

Using Context can increase re‑render frequency; use it for global data like themes or user info.

Avoid overusing Context for data that changes frequently.

4. Refs: Direct access to child components or DOM elements

Refs provide a way to reference a child component or a DOM node directly.

Example

<code>// ParentComponent.jsx
import React, { useRef } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.focusInput();
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
};

export default ParentComponent;

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

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

  useImperativeHandle(ref, () => ({
    focusInput() {
      inputRef.current.focus();
    }
  }));

  return (
    <div>
      <input ref={inputRef} type="text" />
    </div>
  );
});

export default ChildComponent;</code>

Analysis

The parent creates a ref with useRef and passes it to the child. The child uses useImperativeHandle to expose a focusInput method, which the parent calls on button click.

Notes

Refs break React’s one‑way data flow and should be used sparingly.

Typical use cases include accessing DOM elements or integrating third‑party libraries.

5. Hooks: Modern state and communication tools (React 16.8+)

Hooks such as useReducer and useContext enable concise state management and sharing without classes.

Example: useReducer + useContext

<code>// MyContext.js
import React, { createContext, useReducer } from 'react';

const MyContext = createContext();

const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};

export const MyProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <MyContext.Provider value={{ state, dispatch }}>
      {children}
    </MyContext.Provider>
  );
};

export default MyContext;

// ParentComponent.jsx
import React from 'react';
import { MyProvider } from './MyContext';
import ChildComponentA from './ChildComponentA';
import ChildComponentB from './ChildComponentB';

const ParentComponent = () => {
  return (
    <MyProvider>
      <ChildComponentA />
      <ChildComponentB />
    </MyProvider>
  );
};

export default ParentComponent;

// ChildComponentA.jsx
import React, { useContext } from 'react';
import MyContext from './MyContext';

const ChildComponentA = () => {
  const { state, dispatch } = useContext(MyContext);
  return (
    <div>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <p>Count: {state.count}</p>
    </div>
  );
};

export default ChildComponentA;

// ChildComponentB.jsx
import React, { useContext } from 'react';
import MyContext from './MyContext';

const ChildComponentB = () => {
  const { state, dispatch } = useContext(MyContext);
  return (
    <div>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <p>Count: {state.count}</p>
    </div>
  );
};

export default ChildComponentB;</code>

Analysis

The MyProvider component creates state with useReducer and supplies it via MyContext.Provider . Both child components consume the context to read and update the shared count.

Notes

useReducer helps manage complex state logic.

Ensure the context value does not change too frequently to avoid excessive re‑renders.

6. Event Emitter: Decoupled component communication

Using an event emitter (e.g., the mitt library) allows unrelated components to communicate through custom events.

Example: mitt

<code>// eventEmitter.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

// ParentComponent.jsx
import React from 'react';
import emitter from './eventEmitter';
import ChildComponentA from './ChildComponentA';
import ChildComponentB from './ChildComponentB';

const ParentComponent = () => {
  return (
    <div>
      <ChildComponentA />
      <ChildComponentB />
    </div>
  );
};

export default ParentComponent;

// ChildComponentA.jsx
import React from 'react';
import emitter from './eventEmitter';

const ChildComponentA = () => {
  const handleClick = () => {
    emitter.emit('customEvent', 'Hello from ChildComponentA');
  };
  return (
    <div>
      <button onClick={handleClick}>Send Message</button>
    </div>
  );
};

export default ChildComponentA;

// ChildComponentB.jsx
import React, { useEffect, useState } from 'react';
import emitter from './eventEmitter';

const ChildComponentB = () => {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const handler = (msg) => setMessage(msg);
    emitter.on('customEvent', handler);
    return () => {
      emitter.off('customEvent', handler);
    };
  }, []);

  return (
    <div>
      <p>{message}</p>
    </div>
  );
};

export default ChildComponentB;</code>

Analysis

ChildComponentA emits a customEvent with a message. ChildComponentB listens for that event, updates its local state, and renders the received message.

Notes

Remove listeners on component unmount to prevent memory leaks.

Event‑based communication is useful for loosely coupled components but can be harder to debug.

7. Redux: Global state management for large apps

Redux provides a predictable container for application state, using actions and reducers to describe state changes.

Example

<code>// store.js
import { createStore } from 'redux';

const initialState = { count: 0 };

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const store = createStore(reducer);
export default store;

// ParentComponent.jsx
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import ChildComponentA from './ChildComponentA';
import ChildComponentB from './ChildComponentB';

const ParentComponent = () => {
  return (
    <Provider store={store}>
      <ChildComponentA />
      <ChildComponentB />
    </Provider>
  );
};

export default ParentComponent;

// ChildComponentA.jsx
import React from 'react';
import { useDispatch } from 'react-redux';

const ChildComponentA = () => {
  const dispatch = useDispatch();
  const handleIncrement = () => {
    dispatch({ type: 'INCREMENT' });
  };
  return (
    <div>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

export default ChildComponentA;

// ChildComponentB.jsx
import React from 'react';
import { useSelector } from 'react-redux';

const ChildComponentB = () => {
  const count = useSelector(state => state.count);
  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
};

export default ChildComponentB;</code>

Analysis

The ParentComponent wraps children with Provider to supply the Redux store. ChildComponentA dispatches increment actions, while ChildComponentB reads the current count via useSelector .

Notes

Redux shines in large applications; it may be overkill for small projects.

Follow best practices such as immutability and using middleware when appropriate.

Summary and Best Practices

React offers multiple ways to enable component communication, each suited to different scenarios.

Props : Simple one‑way data flow from parent to child.

State Lifting : Share state between sibling components via a common parent.

Context : Propagate data through deep component trees without prop‑drilling.

Refs : Directly access DOM nodes or expose imperative methods.

Hooks : Use useReducer , useContext , and other hooks for flexible state management.

Event Emitter : Decouple components with custom events (e.g., using mitt ).

Redux : Centralized global state for complex, large‑scale applications.

General Recommendations

Prefer simple solutions (Props, State Lifting) before adopting more complex patterns.

Avoid unnecessary state lifting and excessive Context usage to keep components maintainable.

Clean up event listeners to prevent memory leaks.

When using Redux or other state libraries, adhere to their conventions for predictable state changes.

By understanding the strengths and trade‑offs of each method, you can select the most appropriate communication strategy for your React applications.

ReduxReactHooksContextcomponent communicationProps
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

login 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.