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 themessage
as a prop (text
) to theChild
component. - The
Child
component receives thetext
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 ahandleData
function and passes it as a prop (sendData
) to theChild
component. - The
Child
calls thesendData
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 thesetData
function.Sibling2
accesses the updated state via thedata
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 theuseContext
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
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.