1

Here's a class component I'd like to refactor to a functional component using useReducer

export default class FootballMatchesData extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedYear: null,
      matchData: [],
      firstCall: false
    };
  }

  chooseYear = (year) => (e) => {
    this.setState({
      selectedYear: year
    })
    axios.get(`https://website?property=${year}`)
      .then(res => {
        this.setState({ matchData: res.data, firstCall: true })
      })
  }

  render() {
    ...
  }
}

I'm stuck at defining the 'CHOOSE_YEAR' case of my reducer. How would I define that case so that it:

  1. updates selectedYear
  2. makes an api call to https://website?property=${year}, then populates matchData
  3. updates firstCall

Here is my current refactor. https://codesandbox.io/s/gracious-pare-um66h?file=/src/FootballMatches.js

ln09nv2
  • 965
  • 4
  • 19
  • 35

1 Answers1

6

You seem unfamiliar with the reducer pattern. Reducers are pure functions taking a state object and action to apply to that state, and returns the next state object. There are zero side-effects in reducer functions.

Use an useEffect hook to fetch the data when year updates in state. You may not want to also use anchor tags for the year list options since clicking that will likely try to navigate or reload the app/page.

const initialState = {
  selectedYear: null,
  competitions: [],
  firstCall: false
};

const footballReducer = (state, action) => {
  switch (action.type) {
    case "CHOOSE_YEAR":
      return {
        selectedYear: action.year, // <-- save year payload
        firstCall: true
      };

    case "FETCH_BY_YEAR_SUCCESS":
      return {
        ...state, // <-- copy existing state
        competitions: action.competitions // <-- save competitions payload
      };
    default:
      throw new Error();
  }
};

const FootballMatches = () => {
  const [state, dispatchFootball] = useReducer(footballReducer, initialState);

  const yearChooseHandler = (year) => {
    dispatchFootball({ type: "CHOOSE_YEAR", year });
  };

  useEffect(() => {
    if (state.year) { // <-- ensure year value is truthy since null on initial render
      axios.get(`https://website?property=${state.year}`).then((res) => { // <-- access state.year for URL
        dispatchFootball({
          type: "FETCH_BY_YEAR_SUCCESS",
          competitions: res.data,
          firstCall: true
        });
      }
    });
  }, [state.year]); // <-- year dependency

  let years = [2011, 2012, 2013, 2014, 2015, 2016, 2017];
  return (
    <div>
      <div>Select Year</div>
      <ul>
        {years.map((year, idx) => {
          return (
            <li
              onClick={() => yearChooseHandler(year)} // <-- fix callback so it isn't invoked immediately and cause infinite render looping
              key={idx}
            >
              {year}
            </li>
          );
        })}
      </ul>

      ...
    </div>
  );
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • That's a very helpful reply. I really appreciate the comments in the code as well. Thank you! – ln09nv2 Oct 18 '20 at 23:45
  • Can I make API request in `yearChooseHandler` method right after dispatchFootball call? Or it is a bad practice? – Shahin Apr 26 '21 at 22:28
  • 1
    @Shahin I don't think making an API call after `dispatchFootball` in `yearChooseHandler` would be an issue. I would probably factor it into a function and call it versus placing all the fetching logic in the callback. – Drew Reese Apr 26 '21 at 23:40
  • How? AFAIK, `useReducer` hook does not offer callback functionality like `useState` does, which we could make API call in callback function. Can you please show example with `useReducer`? – Shahin Apr 27 '21 at 08:13
  • @Shahin Oh, sorry, I thought you meant literally on the next line *after* calling `dispatchFootball({ type: "CHOOSE_YEAR", year });`. If I know understand your question you are looking for something like a class-based component's `this.setState` callback that is called *after* the state is updated. In functional components hooks don't do this, but the common pattern is to use an `useEffect` hook with a dependency on the state you want the effect to be run after. If this is something you do often you could create a custom `useReducerWithCallback` hook. – Drew Reese Apr 27 '21 at 15:47
  • Yes, you were right, I meant literally on the next line after calling `dispatchFootball({ type: "CHOOSE_YEAR", year });` Please checkout [this] (https://codesandbox.io/s/immutable-bash-b4i07?file=/src/App.js) codesandbox. I put comments on top of the file. – Shahin Apr 27 '21 at 20:49
  • 1
    @Shahin Ah, ok, thanks for clarifying. Yeah, between the two, your option one with `useEffect` with dependency on the `state.country` is IMHO the better of the two. It follows from the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) and allows `onCountrySelect` to just be responsible for updating the country in state, and the second `useEffect`'s callback is responsible for fetching and updating `state.cities` in response. – Drew Reese Apr 27 '21 at 21:06
  • Got it. Thank you very much! – Shahin Apr 27 '21 at 21:14