Skip to main content

Best Ways To Pass Data Between Components On React

· 7 min read
Sivabharathy

Passing data between components in React is an essential part of building applications. Depending on your app's complexity and the relationship between components, React provides multiple strategies for sharing data effectively. This article will explain the best practices for passing data in React with detailed examples and use cases.


1. Parent to Child: Using Props

The most common way to pass data in React is from a parent component to a child component using props. Props are read-only and provide a straightforward way to pass data down the component tree.

Example:

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

return <Child text={message} />;
};

const Child = ({ text }) => {
return <p>{text}</p>;
};

export default Parent;

Explanation:

  • The Parent component passes the message as a prop (text) to the Child component.
  • The Child component receives the text prop and renders it.

When to Use:

  • When there is a direct parent-to-child relationship.
  • For simple, one-way data flow.

2. Child to Parent: Using Callback Functions

To send data from a child component to a parent component, pass a callback function from the parent to the child as a prop. The child component can call this function and pass data as an argument.

Example:

const Parent = () => {
const handleData = (data) => {
console.log("Received from child:", data);
};

return <Child sendData={handleData} />;
};

const Child = ({ sendData }) => {
const sendMessage = () => {
sendData("Hello from Child!");
};

return <button onClick={sendMessage}>Send Data to Parent</button>;
};

export default Parent;

Explanation:

  • The Parent component defines a handleData function and passes it as a prop (sendData) to the Child component.
  • The Child calls the sendData function when the button is clicked, passing the message "Hello from Child!".

When to Use:

  • When the child component needs to send data or trigger actions in the parent.
  • Ideal for event handling, such as form submissions or button clicks.

3. Sibling to Sibling: Using a Shared Parent

When sibling components need to share data, the common approach is to lift the shared state to their parent component. The parent manages the state and passes the necessary data and callbacks to the siblings.

Example:

const Parent = () => {
const [data, setData] = React.useState("");

return (
<div>
<Sibling1 setData={setData} />
<Sibling2 data={data} />
</div>
);
};

const Sibling1 = ({ setData }) => {
return (
<button onClick={() => setData("Data from Sibling 1")}>
Send Data to Sibling 2
</button>
);
};

const Sibling2 = ({ data }) => {
return <p>Received: {data}</p>;
};

export default Parent;

Explanation:

  • The Parent component manages the shared state (data).
  • Sibling1 updates the state by calling the setData function.
  • Sibling2 accesses the updated state via the data prop.

When to Use:

  • When sibling components need to communicate or share state.
  • Keeps the data flow centralized and easier to manage.

4. Using Context API for Global State Management

When multiple components need to access the same data but are deeply nested, React's Context API helps avoid "prop drilling" (passing props through every intermediate component).

Example:

import React, { createContext, useContext, useState } from "react";

const DataContext = createContext();

const Parent = () => {
const [data, setData] = useState("Shared Data");

return (
<DataContext.Provider value={{ data, setData }}>
<Child />
</DataContext.Provider>
);
};

const Child = () => {
const { data, setData } = useContext(DataContext);

return (
<div>
<p>{data}</p>
<button onClick={() => setData("Updated Data!")}>Update Data</button>
</div>
);
};

export default Parent;

Explanation:

  • The DataContext provides a shared state (data) and a function (setData) to update it.
  • Any component within the DataContext.Provider can access these values using the useContext hook.

When to Use:

  • When the state or data needs to be shared across many levels of the component tree.
  • Suitable for themes, authentication, or global settings.

5. Using State Management Libraries (Redux, Zustand, etc.)

For large-scale applications, where state needs to be shared across many components or pages, state management libraries like Redux, Zustand, or MobX are often used.

Example with Redux:

// store.js
import { configureStore, createSlice } from "@reduxjs/toolkit";

const dataSlice = createSlice({
name: "data",
initialState: "Shared Redux State",
reducers: {
updateData: (state, action) => action.payload,
},
});

export const { updateData } = dataSlice.actions;
export default configureStore({ reducer: dataSlice.reducer });

// Component
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateData } from "./store";

const Component1 = () => {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(updateData("Updated Redux State!"))}>
Update Data
</button>
);
};

const Component2 = () => {
const data = useSelector((state) => state);
return <p>{data}</p>;
};

When to Use:

  • When the app has a complex structure with a lot of shared or global state.
  • Ideal for scalable, enterprise-level applications.

6. Passing Data via URL Parameters (React Router)

For routing-based communication between components, data can be passed through URL parameters or query strings.

Example:

// App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Page1 from "./Page1";
import Page2 from "./Page2";

const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Page1 />} />
<Route path="/page2/:data" element={<Page2 />} />
</Routes>
</BrowserRouter>
);
};

// Page1.js
import { useNavigate } from "react-router-dom";

const Page1 = () => {
const navigate = useNavigate();

return (
<button onClick={() => navigate("/page2/HelloFromPage1")}>Go to Page 2</button>
);
};

export default Page1;

// Page2.js
import { useParams } from "react-router-dom";

const Page2 = () => {
const { data } = useParams();

return <p>Received: {data}</p>;
};

export default Page2;

When to Use:

  • For passing data between pages in a single-page application.
  • Useful for navigation-based apps.

7. Custom Hooks for Reusable Logic

To encapsulate reusable stateful logic, you can create a custom hook. Custom hooks are especially useful when the same data or behavior is needed in multiple components.

Example:

const useSharedData = () => {
const [data, setData] = React.useState("Shared Hook Data");
return { data, setData };
};

const Component1 = () => {
const { data, setData } = useSharedData();

return <button onClick={() => setData("Updated Hook Data!")}>Update</button>;
};

const Component2 = () => {
const { data } = useSharedData();

return <p>{data}</p>;
};

When to Use:

  • When you want to encapsulate and reuse stateful logic across components.

Choosing the Best Approach

ScenarioBest Approach
Parent-to-child communicationProps
Child-to-parent communicationCallback functions
Sibling-to-sibling communicationShared parent state
Deeply nested componentsContext API
Global state for large appsState management libraries
Navigation between pagesURL parameters or query strings
Reusable stateful logicCustom hooks

By understanding the relationship between your components and the complexity of your app, you can select the right approach for passing data.