Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to manipulate context - attach function to context or wrap dispatch in hook?

I'm wondering what the recommended best practice is for manipulating and exposing the new React Context.

The easiest way to manipulate context state seems to be to just attach a function to the context that either dispatches (usereducer) or setstate (useState) to change its internal value once called.

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);

  return (
    <Context.Provider
      value={{
        todos: state.todos,
        fetchTodos: async id => {
          const todos = await getTodos(id);
          console.log(id);
          dispatch({ type: "SET_TODOS", payload: todos });
        }
      }}
    >
      {children}
    </Context.Provider>
  );
};

export const Todos = id => {
  const { todos, fetchTodos } = useContext(Context);
  useEffect(() => {
    if (fetchTodos) fetchTodos(id);
  }, [fetchTodos]);
  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

I was however told exposing and using the react context object directly is probably not a good idea, and was told to wrap it inside a hook instead.

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);

  return (
    <Context.Provider
      value={{
        dispatch,
        state
      }}
    >
      {children}
    </Context.Provider>
  );
};

const useTodos = () => {
  const { state, dispatch } = useContext(Context);
  const [actionCreators, setActionCreators] = useState(null);

  useEffect(() => {
    setActionCreators({
      fetchTodos: async id => {
        const todos = await getTodos(id);
        console.log(id);
        dispatch({ type: "SET_TODOS", payload: todos });
      }
    });
  }, []);

  return {
    ...state,
    ...actionCreators
  };
};

export const Todos = ({ id }) => {
  const { todos, fetchTodos } = useTodos();
  useEffect(() => {
    if (fetchTodos && id) fetchTodos(id);
  }, [fetchTodos]);

  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

I have made running code examples for both variants here: https://codesandbox.io/s/mzxrjz0v78?fontsize=14

So now I'm a little confused as to which of the 2 ways is the right way to do it?

like image 895
Dac0d3r Avatar asked Apr 21 '19 21:04

Dac0d3r


People also ask

How do you use Context hook?

Syntax: const authContext = useContext(initialValue); The useContext accepts the value provided by React. createContext and then re-render the component whenever its value changes but you can still optimize its performance by using memoization.

Can I use useContext in custom hook?

Create the useContextInside of this custom hook, we'll be using the useContext hook, that allows us to access both the theme and the setTheme function outside of this file. If useContext fails to create a context, it'll return undefined because we forgot the wrap our App or component in a ThemeProvider.

What is Context hook?

“useContext” hook is used to create common data that can be accessed throughout the component hierarchy without passing the props down manually to each level. Context defined will be available to all the child components without involving “props”.

How do you use Context in useMemo?

Memoize the Context values with useMemo The simplest way to optimize the Context is to useContext in nested components inside App. js . When Wrap the Context value inside of useMemo, all values will be memoized, and useMemo will only recompute the memoized value when one of the dependencies has changed.


1 Answers

There is absolute no problem with using useContext directly in a component. It however forces the component which has to use the context value to know what context to use.

If you have multiple components in the App where you want to make use of TodoProvider context or you have multiple Contexts within your app , you simplify it a little with a custom hook

Also one more thing that you must consider when using context is that you shouldn't be creating a new object on each render otherwise all components that are using context will re-render even though nothing would have changed. To do that you can make use of useMemo hook

const Context = React.createContext<{ todos: any; fetchTodos: any }>(undefined);

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);
  const context = useMemo(() => {
    return {
      todos: state.todos,
      fetchTodos: async id => {
        const todos = await getTodos(id);
        console.log(id);
        dispatch({ type: "SET_TODOS", payload: todos });
      }
    };
  }, [state.todos, getTodos]);
  return <Context.Provider value={context}>{children}</Context.Provider>;
};

const getTodos = async id => {
  console.log(id);
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/todos/" + id
  );
  return await response.json();
};
export const useTodos = () => {
  const todoContext = useContext(Context);
  return todoContext;
};
export const Todos = ({ id }) => {
  const { todos, fetchTodos } = useTodos();
  useEffect(() => {
    if (fetchTodos) fetchTodos(id);
  }, [id]);
  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

Working demo

EDIT:

Since getTodos is just a function that cannot change, does it make sense to use that as update argument in useMemo?

It makes sense to pass getTodos to dependency array in useMemo if getTodos method is changing and is called within the functional component. Often you would memoize the method using useCallback so that its not created on every render but only if any of its dependency from enclosing scope changes to update the dependency within its lexical scope. Now in such a case you would need to pass it as a parameter to the dependency array.

However in your case, you can omit it.

Also how would you handle an initial effect. Say if you were to call `getTodos´ in useEffect hook when provider mounts? Could you memorize that call as well?

You would simply have an effect within Provider that is called on initial mount

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);
  const context = useMemo(() => {
    return {
      todos: state.todos,
      fetchTodos: async id => {
        const todos = await getTodos(id);
        console.log(id);
        dispatch({ type: "SET_TODOS", payload: todos });
      }
    };
  }, [state.todos]);
  useEffect(() => {
      getTodos();
  }, [])
  return <Context.Provider value={context}>{children}</Context.Provider>;
};
like image 152
Shubham Khatri Avatar answered Dec 26 '22 00:12

Shubham Khatri