Reputation: 1333
What is the recommended pattern for doing a setState on a parent from a child component.
var Todos = React.createClass({
getInitialState: function() {
return {
todos: [
"I am done",
"I am not done"
]
}
},
render: function() {
var todos = this.state.todos.map(function(todo) {
return <div>{todo}</div>;
});
return <div>
<h3>Todo(s)</h3>
{todos}
<TodoForm />
</div>;
}
});
var TodoForm = React.createClass({
getInitialState: function() {
return {
todoInput: ""
}
},
handleOnChange: function(e) {
e.preventDefault();
this.setState({todoInput: e.target.value});
},
handleClick: function(e) {
e.preventDefault();
//add the new todo item
},
render: function() {
return <div>
<br />
<input type="text" value={this.state.todoInput} onChange={this.handleOnChange} />
<button onClick={this.handleClick}>Add Todo</button>
</div>;
}
});
React.render(<Todos />, document.body)
I have an array of todo items which is maintained in the parent's state.
I want to access the parent's state and add a new todo item, from the TodoForm
's handleClick
component.
My idea is to do a setState on the parent, which will render the newly added todo item.
Upvotes: 133
Views: 178028
Reputation: 156
As of v18.2, The React official documentation shows you how to do this with the combination of useReducer
and useContext
.
https://react.dev/learn/scaling-up-with-reducer-and-context
Here is an idea how to implement:
import React, { createContext, useContext, useReducer, useState } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
function useTasks() { return useContext(TasksContext); }
function useTasksDispatch() { return useContext(TasksDispatchContext); }
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, { id: action.id, text: action.text }];
}
…
}
}
let nextId = 0;
function AddTask() {
const [text, setText] = useState('');
const dispatch = useTasksDispatch();
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => {
setText('');
dispatch({type: 'added', id: nextId++, text: text })
}}>Add</button>
</>
)
}
function TaskList() {
const tasks = useTasks();
return (
{tasks.map(task => (<div key={task.id}>{task.text}</div))}
);
}
export default function TaskApp() {
return (
<TasksProvider>
<AddTask />
<TaskList />
</TasksProvider>
);
}
Upvotes: 1
Reputation: 304
For anyone using useState
inside Arrow Functional Components
with TypeScript
enabled, here's a simple example of how you can pass the parent's state setter function to the child, and invoke it to set the parent's state at the appropriate time from the child-
Parent component:
import {useState} from "react";
const ParentComponent = () => {
const [hasParentStateChange, setHasParentStateChange] = useState<boolean>(false);
return (
<div>
Parent's view: {String(hasParentStateChange)}
<ChildComponent
hasParentStateChange={hasParentStateChange}
setHasParentStateChange={setHasParentStateChange} // <---- Passing parent's state setter over
/>
</div>
);
}
Child component:
interface PropsDefinition {
hasParentStateChange: boolean;
setHasParentStateChange(data: boolean): void;
}
const ChildComponent = (props: PropsDefinition) => {
return (
<div>
<div>
Child's view: {String(props.hasParentStateChange)}
</div>
<button
onClick={() => props.setHasParentStateChange(!props.hasParentStateChange)} // <---- Invoking parent's state setter
>
Toggle parent state from child
</button>
</div>
);
}
Upvotes: 8
Reputation: 90
If you are working with a class component as parent, one very simple way of passing a setState to a child is by passing it within an arrow function. This works as it sets a hoisted environment that can be passed around:
class ClassComponent ... {
modifyState = () =>{
this.setState({...})
}
render(){
return <><ChildComponent parentStateModifier={modifyState} /></>
}
}
Upvotes: 1
Reputation: 2441
For those who are maintaining state with the React Hook useState
, I adapted the above suggestions to make a demo slider App below. In the demo app, the child slider component maintains the parent's state.
The demo also uses useEffect
hook. (and less importantly, useRef
hook)
import React, { useState, useEffect, useCallback, useRef } from "react";
//the parent react component
function Parent() {
// the parentState will be set by its child slider component
const [parentState, setParentState] = useState(0);
// make wrapper function to give child
const wrapperSetParentState = useCallback(val => {
setParentState(val);
}, [setParentState]);
return (
<div style={{ margin: 30 }}>
<Child
parentState={parentState}
parentStateSetter={wrapperSetParentState}
/>
<div>Parent State: {parentState}</div>
</div>
);
};
//the child react component
function Child({parentStateSetter}) {
const childRef = useRef();
const [childState, setChildState] = useState(0);
useEffect(() => {
parentStateSetter(childState);
}, [parentStateSetter, childState]);
const onSliderChangeHandler = e => {
//pass slider's event value to child's state
setChildState(e.target.value);
};
return (
<div>
<input
type="range"
min="1"
max="255"
value={childState}
ref={childRef}
onChange={onSliderChangeHandler}
></input>
</div>
);
};
export default Parent;
Upvotes: 26
Reputation: 2531
In your parent, you can create a function like addTodoItem
which will do the required setState and then pass that function as props to the child component.
var Todos = React.createClass({
...
addTodoItem: function(todoItem) {
this.setState(({ todos }) => ({ todos: { ...todos, todoItem } }));
},
render: function() {
...
return <div>
<h3>Todo(s)</h3>
{todos}
<TodoForm addTodoItem={this.addTodoItem} />
</div>
}
});
var TodoForm = React.createClass({
handleClick: function(e) {
e.preventDefault();
this.props.addTodoItem(this.state.todoInput);
this.setState({todoInput: ""});
},
...
});
You can invoke addTodoItem
in TodoForm's handleClick. This will do a setState on the parent which will render the newly added todo item. Hope you get the idea.
Upvotes: 119
Reputation: 362
These are all essentially correct, I just thought I would point to the new(ish) official react documentation which basically recommends:-
There should be a single “source of truth” for any data that changes in a React application. Usually, the state is first added to the component that needs it for rendering. Then, if other components also need it, you can lift it up to their closest common ancestor. Instead of trying to sync the state between different components, you should rely on the top-down data flow.
See https://reactjs.org/docs/lifting-state-up.html. The page also works through an example.
Upvotes: 15
Reputation: 21757
I found the following working and simple solution to pass arguments from a child component to the parent component:
//ChildExt component
class ChildExt extends React.Component {
render() {
var handleForUpdate = this.props.handleForUpdate;
return (<div><button onClick={() => handleForUpdate('someNewVar')}>Push me</button></div>
)
}
}
//Parent component
class ParentExt extends React.Component {
constructor(props) {
super(props);
var handleForUpdate = this.handleForUpdate.bind(this);
}
handleForUpdate(someArg){
alert('We pass argument from Child to Parent: \n' + someArg);
}
render() {
var handleForUpdate = this.handleForUpdate;
return (<div>
<ChildExt handleForUpdate = {handleForUpdate.bind(this)} /></div>)
}
}
if(document.querySelector("#demo")){
ReactDOM.render(
<ParentExt />,
document.querySelector("#demo")
);
}
Upvotes: 2
Reputation: 4770
You could create an addTodo function in the parent component, bind it to that context, pass it to the child component and call it from there.
// in Todos
addTodo: function(newTodo) {
// add todo
}
Then, in Todos.render, you would do
<TodoForm addToDo={this.addTodo.bind(this)} />
Call this in TodoForm with
this.props.addToDo(newTodo);
Upvotes: 11