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
Parentcomponent passes themessageas a prop (text) to theChildcomponent. - The
Childcomponent receives thetextprop 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
Parentcomponent defines ahandleDatafunction and passes it as a prop (sendData) to theChildcomponent. - The
Childcalls thesendDatafunction 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
Parentcomponent manages the shared state (data). Sibling1updates the state by calling thesetDatafunction.Sibling2accesses the updated state via thedataprop.
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
DataContextprovides a shared state (data) and a function (setData) to update it. - Any component within the
DataContext.Providercan access these values using theuseContexthook.
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
| Scenario | Best Approach |
|---|---|
| Parent-to-child communication | Props |
| Child-to-parent communication | Callback functions |
| Sibling-to-sibling communication | Shared parent state |
| Deeply nested components | Context API |
| Global state for large apps | State management libraries |
| Navigation between pages | URL parameters or query strings |
| Reusable stateful logic | Custom hooks |
By understanding the relationship between your components and the complexity of your app, you can select the right approach for passing data.

