Skip to content

Instantly share code, notes, and snippets.

@ajLapid718
Last active June 18, 2020 22:16
Show Gist options
  • Save ajLapid718/5597d565c3090955c22ae0e2b6a2ca84 to your computer and use it in GitHub Desktop.
Save ajLapid718/5597d565c3090955c22ae0e2b6a2ca84 to your computer and use it in GitHub Desktop.
Redux and React-Redux Overview

React-Redux

Let's say our objective was to make an AJAX request (an HTTP GET request, specifically) from our frontend (from a React component, specifically) to our Express server on the backend. We want to fetch all of the students and then put this array of student objects somewhere so that at any given point in any given component, we would have access to it.

The location, or structure, that will be containing this array of student objects would be one of the values on our Redux store composed of key:value pairs. Please keep in mind that referring to the Redux store as "the store", "the global Redux store", or "the Redux state" are all interchangeable so long as we understand we are referring to the one shared object containing keys and values (just like any other object in JS). This can also be thought of as one shared hash in Ruby or one shared dictionary in Python. In the Redux store, the keys are strings and the values are the most recent return value of the corresponding reducer function.

Now that we know our objective, we have to implement this. Assuming our store (and our Express server) is properly configured and instantiated with the proper combination of reducers and middleware, here's what we'll need.

We need:

  • an action type
  • an action creator
  • a thunk creator
  • a reducer function
// Action Type
const FETCH_ALL_STUDENTS = "FETCH_ALL_STUDENTS";

// Action Creator (Pre-ES6 Syntax)
function fetchAllStudents(students) {
  return {
    type: FETCH_ALL_STUDENTS,
    payload: students
  }
}

// Thunk Creator (Pre-ES6 Syntax)
function fetchAllStudentsThunk() {
  return function(dispatch) {
    return axios
      .get("localhost:3000/api/students")
      .then(res => res.data)
      .then(students => dispatch(fetchAllStudents(students)))
      .catch(err => console.log(err))
    }
}

// Reducer Function (Pre-ES6 Syntax)
function allStudents(state = [], action) {
  switch (action.type) {
    case FETCH_ALL_STUDENTS:
      return action.payload;
    default:
      return state;
    }
}

// Exporting so we can use our thunk in our React component and we can use our reducer function when we set up our store
export fetchAllStudentsThunk;
export default allStudents;

Assuming we set up our store properly, our store now looks like this:

{ allStudents: [] }

Now let's write our React Component.

We need to write a class component that we'll eventually connect to our Redux store so that it can "be provided" the Redux store from the Provider wrapper/component. Assume that we imported our fetchAllStudentsThunk properly. Also, let's pretend that a parent component of AllStudents passed down props (headCount) into it like so:

...in some other parent component this happens: <AllStudents headCount={30} />....

class AllStudents extends Component {
  constructor() {
    super();
  }

  componentDidMount() {
    this.props.watermelon(); // this grabs our data from the backend and eventually causes the reducer function to update the Redux store with the data of students;
  }

  render() {
  // if we console.log(this.props) here, we'd see an object that looked like this on the initial render: 
  { 
    headCount: 30, 
    banana: [], 
    watermelon: our anonymous function and its function signature 
  }

		// if we console.log(this.props) here, after the componentDidMount() runs and leads to a re-render (since the store updated with the array of student objects [trace the pathway and outcome of invoking the fetchStudentThunk() from axios call to dispatch to the reducer function]), our props object would like this:
  { 
    headCount: 30, 
    banana: [{id: 1, name: AJ}, {id: 2, name: Brian}, {id: 3, name: Carl}, {id: 4, name: Adrian}],
    watermelon: our anonymous function and its function signature
  }
  
  this.props.banana.map(do map things, which does nothing on the initial render, but will be able to map over the data after the componentMounts);
  }
}

function mapStateToProps(state) {
  // if we console.log(state) here, we'd see our entire Redux object/store;
  return { 
    banana: state.allStudents
  }
}

function mapDispatchToProps(dispatch) {
  return {
    watermelon: () => dispatch(fetchAllStudentsThunk())
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(AllStudents);

Now that we've gotten all of our students, let's see what would happen if we wanted to add a student to our array of students. Assuming we have the proper Express endpoint setup, we'd have to do something like this.

We'd have to modify our reducer function to include a new case statement.

function allStudents(state = [], action) {
  switch (action.type) {
    case FETCH_ALL_STUDENTS:
      return action.payload;
    case ADD_NEW_STUDENT:
      return [...state, action.payload]; // spread out the old array of values and include this new single student that we just added (you can also use Object.assign for this as well);
    default:
      return state;
  }
}

Here's the action type, action creator, and thunk creator for that.

// Action Type
const ADD_NEW_STUDENT = "ADD_NEW_STUDENT";

// Action Creator
function addNewStudent(newStudent) {
  return {
    type: ADD_NEW_STUDENT,
    payload: newStudent
  }
}

// Thunk Creator
function addNewStudentThunk(student) {
  return function(dispatch) {
    return axios
      .post("localhost:3000/api/students", student)
      .then(res => res.data)
      .then(newStudent => dispatch(addNewStudent(newStudent)))
    }
}

// Reducer (for clarity);
function allStudents(state = [], action) {
  switch (action.type) {
    case FETCH_ALL_STUDENTS:
      return action.payload;
    case ADD_NEW_STUDENT:
      return [...state, action.payload]; // spread out the old array of values and include this new single student that we just added (you can also use Object.assign for this as well);
     default:
       return state;
  }
}

// Exports
export addNewStudentThunk; // this is a new line --- so that we can use this in our component, most likely on the handleSubmit function of our NewStudentFormComponent;
export default allStudents; // we already did this before, this line is here for clarity;

Now let's say we have a NewStudentFormComponent and we have our imports of connect as well as our addNewStudentThunk;

class NewStudentFormComponent extends Component {
  constructor() {
    super();
    this.state = { name: "Brent" }
  }
  handleSubmit() {
    this.props.mango(this.state);
  }

  render()~~~
}

function mapDispatchToProps(dispatch) {
  return {
    mango: (studentToPost) => dispatch(addNewStudentThunk(studentToPost))
  }
}

export default connect(null, mapDispatchToProps)(NewStudentFormComponent); // we pass in null if we don't use mapStateToProps;

This call of this.props.mango(this.state) will invoke the anonymous function with a parameter of studentToPost and will then dispatch the addNewStudentThunk with the object we held on state.

this.props.mango(this.state) ===> dispatch(addNewStudentThunk(studentToPost which is equivalent to this.state)) ===> reduxThunkMiddleware hijacks this action, invokes it (see the body of the addNewStudentThunk), runs the body of the code, and then the student from the local React state will then be dispatched to the reducer function ===> the reducer function will handle the appropriate action.type ===> the global Redux store will then be updated such that our Redux store now looks like this:

// Redux store, the current version/snapshot;
{
	allStudents: [{id: 1, name: "AJ"}, {id: 2, name: "Brian"}, {id: 3, name: "Carl"}, {id: 4, name: "Adrian"}], {id: 5, name: "Brent"}]
}

So, once again, here's the flow for that.

  • invoke the anonymous function associated with this.props.mango, passing in the information/object from the React component's local state object
  • that will lead to running the body of that anonymous function, which proceeds to dispatch the addNewStudentThunk(studentToPost aka this.state) invoked
  • from there, the addNewStudentThunk (invoked) will be intercepted by the Redux Thunk Middleware (because this is a function, Redux Thunk will invoke it for us)
  • then, the addNewStudentThunk's inner function will make an AJAX request to POST our new student to the database
  • then, we'll dispatch the addNewStudent action creator/function, passing in the newly, freshly posted student as the payload;
  • this action will be sent over to the store (because it is a plain object at this point in the sequence of events, Redux Thunk let's it slide by unscathed)
  • the reducer will look for the type, see that the action.type is ADD_NEW_STUDENT
  • this will cause the switch statement to catch that, handle that, and process it such that it returns the previous state spread out with the addition of this new student (aka the action.payload)
  • now our Redux Store, which has a key titled "allStudents" and an associated value of whatever the latest return value from the allStudents reducer function is,

went from this:

{ allStudents: [{id: 1, name: "AJ"}, {id: 2, name: "Brian"}, {id: 3, name: "Carl"}, {id: 4, name: "Adrian"}] };

to this:

{ allStudents: [{id: 1, name: "AJ"}, {id: 2, name: "Brian"}, {id: 3, name: "Carl"}, {id: 4, name: "Adrian"}], {id: 5, name: "Brent"}] };

If we took all of the above, and in addition to that did the same exact thing (same underlying business logic, different variables and syntax) except for campuses, we'd have a Redux store that looks like this (assuming we retrieved an array of campuses in a similar way for its corresponding Express endpoint):

{
  allStudents: [{id: 1, name: "AJ"}, {id: 2, name: "Brian"}, {id: 3, name: "Carl"}, {id: 4, name: "Adrian"}], {id: 5, name: "Brent"}],
  allCampuses: [{id:1, name: "Hunter", {id: 2, name: "NYU"}]
}

We'd have to have separate reducer functions if we wanted to maintain the state of singleStudent and singleCampus. With those included, our store would look something like this (NOTE: whatever we define as our initial state in our reducer function will dictate the shape of our Redux store's values and our choices for initial state should be the bare minimum representation of that kind of data (so an array for a list of elements, an object for a single element, an integer of possibly 0 when dealing with integers, etc)):

{
  allStudents: [{id: 1, name: "AJ"}, {id: 2, name: "Brian"}, {id: 3, name: "Carl"}, {id: 4, name: "Adrian"}], {id: 5, name: "Brent"}],
  allCampuses: [{id:1, name: "Hunter", {id: 2, name: "NYU"}],
  singleStudent: {},
  singleCampus: {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment