Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React context useReducer is not updating correctly

I am having trouble getting reducer to work with React context. In buttonbar.js, there are two buttons that are supposed to update the state. The state will be updated by filtering the data in the current. The buttons are being clicked and I don't get any errors, but it's also not doing anything. I think the issue is with the reducer.

context.js

import React, { useState, useEffect } from "react";
import * as moment from "moment";
import axios from "axios";

export const Context = React.createContext();

const url = "https://projects.fivethirtyeight.com/polls/polls.json";

export const filterReducer = (state, action) => {
  switch (action.type) {
    case "SHOW_ALL":
      return state.polls;
    case "SHOW_APPROVAL":
      return state.polls.filter(e => e.type === "trump-approval");
    default:
      return state.polls;
  }
};

export function Provider({ children }) {
  let intialState = {
    polls: [],
    dispatch: action => this.setState(state => filterReducer(state, action))
  };

  const [state, setState, dispatch] = useState(intialState);

  useEffect(() => {
    var dateRange = moment()
      .subtract(7, "days")
      .calendar();

    axios
      .get(url)
      .then(res => {
        setState({
          polls: res.data
            .filter(e => Date.parse(e.endDate) >= Date.parse(dateRange))
            .reverse()
        });
      }, [])
      .catch(error => console.log(error));
  }, []);

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

// export const Consumer = Context.Consumer;

buttonbar.js

import React, { useContext, useState, useEffect, useReducer } from "react";
import { Context, filterReducer } from "../context";

const ButtonBar = () => {
  const [state, setState] = useContext(Context);
  const [filter, dispatch] = useReducer(filterReducer, state);

  const showAll = () => {
    dispatch({ type: "SHOW_ALL" });
    console.log("showAll clicked");
  };
  const showApproval = () => {
    dispatch({ type: "SHOW_APPROVAL" });
    console.log("showApproval clicked");
  };

  return (
    <div class="mb-2">
      <button class="btn btn-primary btn-sm" name="all" onClick={showAll}>
        All
      </button>{" "}
      <button
        class="btn btn-primary btn-sm"
        name="trump approval"
        onClick={showApproval}
      >
        Trump Approval
      </button>
    </div>
  );
};

export default ButtonBar;

like image 943
jhaywoo8 Avatar asked Jun 10 '19 19:06

jhaywoo8


2 Answers

Your using the useReducer Hook incorrectly, just because you are using the useReducer hook in your component, does not mean you are updating the global context state.

So in your buttonbar.js

  const [filter, dispatch] = useReducer(filterReducer, state);

  const showAll = () => {
    dispatch({ type: "SHOW_ALL" });
    console.log("showAll clicked");
  };
  const showApproval = () => {
    dispatch({ type: "SHOW_APPROVAL" });
    console.log("showApproval clicked");
  };

You are updating your state correctly using a reducer but it will only update local component state not the global context state.

This will seem counter-intuitive if your are coming from redux.

In context the state is contained and changed in the parent component so simply move the above code to the parent component, then access it through context.

export function Provider({ children }) {
  let intialState = {
    polls: [],
    dispatch: action => this.setState(state => filterReducer(state, action))
  };

  // 2 args not 3
  const [state, setState] = useState(intialState);

  const [filter, dispatch] = useReducer(filterReducer, state);

  const showAll = () => {
    dispatch({ type: "SHOW_ALL" });
    console.log("showAll clicked");
  };
  const showApproval = () => {
    dispatch({ type: "SHOW_APPROVAL" });
    console.log("showApproval clicked");
  };

pass the state and functions to the value prop

   <Context.Provider value={{
                          showAllProp: () => showAll(),
                          showApprovalProp: () => showApproval(),
                          filterProp: filter }}>
      {children}
    </Context.Provider>

Then you can access these values and functions in the child component with the value props.

   const context = useContext(Context);  

  <button class="btn btn-primary btn-sm" name="all" onClick={context.showAllProp}>
    All
  </button>{" "}
  <button
    class="btn btn-primary btn-sm"
    name="trump approval"
    onClick={context.showApprovalProp}
  >

This is essentially how you connect your context with your components.

like image 130
iqbal125 Avatar answered Oct 18 '22 02:10

iqbal125


There are a few things, you are not doing correctly.

First, you are using initialState with a dispatch method and you are instead trying to get this dispatch value from useState using the third argument which is incorrect

Second, Since you are using the reducer pattern, its better to make use if useReducer hook

Third, you must never filter the data in reducer otherwise the next time you want to show all the data, the complete data will be lost and only the filtered data will remain. Instead you must have selectors for it.

Relevant code:

import React, {
  useEffect,
  useContext,
  useReducer,
  useMemo,
  useState
} from "react";
import ReactDOM from "react-dom";

import "./styles.css";
import moment from "moment";
import axios from "axios";

export const Context = React.createContext();

const url = "https://projects.fivethirtyeight.com/polls/polls.json";

export const filterReducer = (state, action) => {
  switch (action.type) {
    case "ADD_POLLS":
      console.log(action.payload);
      return action.payload;
    default:
      return state.polls;
  }
};

export function Provider({ children }) {
  const [state, dispatch] = useReducer(filterReducer);

  useEffect(() => {
    var dateRange = moment()
      .subtract(7, "days")
      .calendar();

    axios
      .get(url)
      .then(res => {
        dispatch({
          type: "ADD_POLLS",
          payload: res.data
            .filter(e => Date.parse(e.endDate) >= Date.parse(dateRange))
            .reverse()
        });
      }, [])
      .catch(error => console.log(error));
  }, []);

  return (
    <Context.Provider value={[state, dispatch]}>{children}</Context.Provider>
  );
}
const ButtonBar = () => {
  const [polls] = useContext(Context);
  const [state, setState] = useState(polls);
  useEffect(() => {
    setState(polls);
  }, [polls]);
  const filterResult = useMemo(() => {
    return filter => {
      switch (filter) {
        case "SHOW_ALL":
          setState(polls);
          break;
        case "SHOW_APPROVAL":
          setState(polls.filter(e => e.type === "trump-approval"));
          break;
        default:
          return;
      }
    };
  }, [polls]);

  return (
    <div class="mb-2">
      <button
        class="btn btn-primary btn-sm"
        name="all"
        onClick={() => filterResult("SHOW_ALL")}
      >
        All
      </button>{" "}
      <button
        class="btn btn-primary btn-sm"
        name="trump approval"
        onClick={() => filterResult("SHOW_APPROVAL")}
      >
        Trump Approval
      </button>
      <div>{(state || []).length}</div>
      <pre>{JSON.stringify(state, null, 4)}</pre>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider>
    <ButtonBar />
  </Provider>,
  rootElement
);

Working demo

like image 20
Shubham Khatri Avatar answered Oct 18 '22 03:10

Shubham Khatri