Michael
Michael

Reputation: 57

How do I update a single item in list with React Context?

I really like react context, but I think it's missing something (or maybe I don't know how to do it)

Say I have a list of todos and it's corresponding provider as

const Home = () => (
  <div className="container">
    <TodosProvider>
      <TodosList />
    </TodosProvider>
  </div>
)

const TodosList = () => {
  const { todos } = useTodos();

  return (
    <>
      {todos.map((todo, idx) => (
          <SingleTodo />
      ))}
    </>
  )
}

And in another file


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

const TodosContext = createContext({});


export const TodosProvider = ({ children }) => {

    const [todos, setTodos] = useState([{ text: 'a' }, { text: 'b' }, { text: 'c' }])

    return (
        <TodosContext.Provider value={{ todos }}>
            {children}
        </TodosContext.Provider>
    )

}


export const useTodos = () => {
    const todos = useContext(TodosContext)
    return todos
}

How can I update a single todo inside the SingleTodo without:

1) Passing the map idx as a property to the SingleTodo and then from SingleTodo call a method of the TodosList provider passing the idx as a parameter

2) Giving an artificial id property to the todo. And then in TodosProvider update the todo that matches with that id.

The reasons for those restrictions are that:

1) Passing down the position of the todo in the rendering as a prop, invalidates the benefits of using context, which is to not have to do prop drilling

2) I don't think it's good to pollute the model with an artificial id just for state management.

I'd like to be able to create a SingleTodoContext and instantiate a SingleTodoProvider in each iteration of the loop

const TodosList = () => {
  const { todos } = useTodos();

  return (
    <>
      {todos.map((todo, idx) => (
        <SingleTodoProvider key={idx} loadFrom={todo}>
          <SingleTodo />
        </SingleTodoProvider>
      ))}
    </>
  )
}

But that doesn't work because the provider would then need to store the loadFrom property as a state, and that would break the sync between the list todo, and the single todo.

So, how do I update a single item inside a list without prop drilling the position of the item in the list? I don't want to use Redux

Upvotes: 2

Views: 2192

Answers (1)

jack.benson
jack.benson

Reputation: 2363

You can pass methods for updating the values in context as part of your context. Here is an example based on your code (sort of all crammed together):

import React from "react";
import "./styles.css";
import { createContext, useContext, useState } from "react";

const TodosContext = createContext({});

export const TodosProvider = ({ children }) => {
  const [todos, setTodos] = useState([
    { text: "a" },
    { text: "b" },
    { text: "c" }
  ]);

  const selectTodo = (todo, idx) => {
    console.log(
      "do something with the todo here and then call setTodos, or something else?",
      todo.text,
      idx
    );

    // setTodos(prev => /* Do something here to update the list */)
  };

  return (
    <TodosContext.Provider value={{ selectTodo, todos }}>
      {children}
    </TodosContext.Provider>
  );
};

export const useTodos = () => {
  const todos = useContext(TodosContext);
  return todos;
};

const Home = () => (
  <div className="container">
    <TodosProvider>
      <TodosList />
    </TodosProvider>
  </div>
);

const SingleTodo = ({ todo, onClick }) => (
  <div>
    {todo.text} <button onClick={() => onClick(todo)}>Click Me!</button>
  </div>
);

const TodosList = () => {
  const { selectTodo, todos } = useTodos();

  return todos.map((todo, idx) => (
    <SingleTodo onClick={todo => selectTodo(todo, idx)} todo={todo} key={idx} />
  ));
};

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <Home />
    </div>
  );
}

Hope that helps!

Upvotes: 3

Related Questions