Pavithra
Pavithra

Reputation: 1333

ReactJS: setState on parent inside child component

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

Answers (9)

user3445724
user3445724

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

dezman
dezman

Reputation: 19358

parentSetState={(obj) => this.setState(obj)}

Upvotes: 4

dian jin
dian jin

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-

Sample

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

Julio Pereira
Julio Pereira

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

NicoWheat
NicoWheat

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

Deepak
Deepak

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.

Fiddle here.

Upvotes: 119

TattyFromMelbourne
TattyFromMelbourne

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

Roman
Roman

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")
    );
}

Look at JSFIDDLE

Upvotes: 2

rallrall
rallrall

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

Related Questions