cicscareers / 320-S20-Track1

Integration Track 1
BSD 3-Clause "New" or "Revised" License
11 stars 3 forks source link

Frontend state management & asynchronous actions #405

Open mattrheault opened 4 years ago

mattrheault commented 4 years ago

Instructions: Just an FYI!

Just food for thought - no action required 😄 👍

Describing the problem & solution

Hello CS320 friends!

With my first FYI post, I wanted to call-out a useful design pattern that will help keep your react components simple & focused on view-logic. Namely, there are a number of components that have async logic & state management in them.

Example - EditMajors.js

The problem: Components have a lot of responsibility already with declaring view states. When components try to also handle network IO + stateful updates, they get complicated fast. Plus, tightly coupling IO logic within components means that said logic cannot be re-used across different parts of your react app.

The solution: Using a flux model async work can be coded & tested in isolation from components, and thus can be easily re-used. Redux is by far the most popular flux-like implementation in react.

Implementing redux in react

Typically, a frontend framework like angular, or ember would have strong opinions on how something like this should work. But with react being a "batteries not included" framework, we have to stitch the pieces together ourselves.

With redux setup, the app might look a bit more like this:

actions/StudentActions.js

Actions dispatch updates to alert redux reducers of state changes.

export const fetchStudent= (dispatch) => () => {
  dispatch({
    action: 'STUDENT_FETCH_STARTED',
  });

  MyHttpClient.get(`https://7jdf878rej.execute-api.us-east-2.amazonaws.com/test/users/students/${data.id}`)
  .then((resp) => {
    dispatch({
      action: 'STUDENT_FETCH_COMPLETED',
      payload: resp,
    });
  }).catch((err) => {
    dispatch({
      action: 'STUDENT_FETCH_FAILED',
      payload: err,
    });
  });
}

reducers/StudentInfo.js

Reducers listen for dispatched updates, & adjust state accordingly.

const INITIAL_STATE = {
  loading: false,
  error: null,
  data: null,
};

const StudentInfo = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case STUDENT_FETCH_STARTED:
      return {
        loading: true,
        error: null,
        data: null,
      };
    case STUDENT_FETCH_COMPLETED:
      return {
        loading: false,
        error: null,
        data: action.payload,
      };
    case STUDENT_FETCH_FAILED:
      return {
        loading: false,
        error: action.payload,
        data: null,
      };
};

components/StudentInfo.js

Our component now selects props from redux state, & hooks up the actions.

import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { updateUser } from '../actions/StudentActions';

const StudentInfo = (props) => {
  useEffect(props.fetchStudent);
  if (props.loading) {
    return null;
  }

  return (
    <div>
      <p>Student's name is {props.data.name}.</p>
    </div>
  );
};

const mapStateToProps = (state) => {
  return {
    loading: state.studentInfo.loading,
    error: state.studentInfo.error,
    data: state.studentInfo.data,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    fetchStudent: (data) => dispatch(StudentActions.fetchStudent()),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(StudentInfo);
powellsl99 commented 4 years ago

This is awesome! I never knew about redux before. I just watched a video on it and it seems perfect. Thanks @mattrheault !!!

mattrheault commented 4 years ago

RE: Priority & strategy for this refactor

IMO this should be a high-priority refactoring project because it will dictate much of how future features will be built. Plus, the longer we wait, the harder the refactor will be. Thankfully, we can tackle this in stages with small PRs targeting 1-component or action at a time.

I can get us started with the first few refactoring PRs. But we'll also want to make sure that any net-new components use redux instead of trying to handle the async logic themselves.