• Stars
    star
    114
  • Rank 308,031 (Top 7 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 6 years ago
  • Updated about 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A bare-bones redux implementation for teaching purposes ๐ŸŽ“

Simple Redux

A bare-bones redux and react-redux implementation for teaching purposes

Table of contents

Why

Simple Redux is intended to teach you the core concepts of Redux.

Who's this for?

This is for developers with experience using Redux with React.

You won't learn how to use actions, reducers, or the connect function. Instead, you'll learn how they work under the hood.

What's included?

Simple Redux implements code from the redux and react-redux packages. This includes createStore, combineReducers, connect, and the Provider component.

The file and function names are purposefully close to the original source code. If you decide to read the Redux code after reading through this example, you'll be in familiar territory.

This README is a walk-through of the Simple Redux code. If you're a maverick that doesn't care for structure, you can dive straight into the source code in the redux and react-redux directories.

The first code to look at in the walk-through is the code that creates a store.

The store

In Redux you create a store by calling createStore with a reducer:

import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

createStore forms a closure which contains the store state (currentState), and returns the store methods:

// redux/createStore.js

export default function createStore(reducer) {
  let currentState
  let listeners = []

  const getState = () => currentState

  const subscribe = listener => {
    listeners.push(listener)
  }

  const dispatch = action => {
    currentState = reducer(currentState, action)
    listeners.forEach(l => l())
  }

  // ..

  return {
    dispatch,
    subscribe,
    getState
  }
}

You can see that dispatch calls the store reducer with an action and the previous state to generate new state:

// redux/createStore.js

export default function createStore(reducer) {
  // ..
  const dispatch = action => {
    currentState = reducer(currentState, action)
    // ..
  }
  // ..
}

That's why a reducer must always return an object. As a reminder, this is what a reducer looks like:

// app/store/todosReducer.js

const initialState = {
  items: []
}

export default function todosReducer(state = initialState, action) {
  if (action.type === 'ADD_TODO') {
    return {
      ...state,
      items: [...state.items, action.payload]
    }
  }
  return state
}

Notice that the reducer state param has a default value of initialState. This value is used when the state is initialized:

// app/store/todosReducer.js

const initialState = {
  items: []
}

export default function todosReducer(state = initialState, action) {
  // ..
  return state
}

The state is initialized in createStore by dispatching an action with a unique type that (hopefully) won't match any cases in the reducer.

// redux/createStore.js

export default function createStore(reducer) {
  let currentState

  const dispatch = action => {
    currentState = reducer(currentState, action)
    // ..
  }

  dispatch({ type: '@@redux/INIT$' })

  return {
    // ..
  }
}

In this initial dispatch call currentState is undefined. So when the reducer is called by dispatch with undefined as the first argument, it will return the default initialState value to create the currentState:

// app/store/todosReducer.js

const initialState = {
  items: []
}

export default function todosReducer(state = initialState, action) {
  // ..
  return state
}

That's all there is to the storeโ€”it's surprisingly simple! But you probably use more than one reducer to create your store. That's where combineReducers comes in.

combineReducers

Instead of using a single root reducer, most apps use multiple reducers by using the combineReducers helper function:

// app/store/reducers.js

import { combineReducers } from 'redux'
import todos from './todosReducer'
import modal from './modalReducer'

const rootReducer = combineReducers({
  todos,
  modal
})

Which generates a single state object:

store.getState() // =>
// { todos: { items: [] }, modal: { } }

combineReducers is pleasingly simple. It returns a function (combination) that calls each reducer with the previous state generated by the reducer, and the action:

// redux/combineReducers.js

export default function combineReducers(reducers) {
  return function combination(state = {}, action) {
    let nextState = {}

    Object.keys(reducers).forEach(key => {
      let reducer = reducers[key]
      let previousStateForKey = state[key]

      nextState[key] = reducer(previousStateForKey, action)
    })

    return nextState
  }
}

That's all the code needed to create a Simple Redux store.

The next part to look at is the code that connects the store to an app.

Connecting a store to React

React projects use the react-redux package to connect a store to a React app.

You do this by creating container components with the connect function:

import { connect } from 'react-redux'

const ModalContainer = connect(mapStateToProps)(Modal)

In order for a container to work you need to render a <Provider /> component somewhere in the component tree above the container.

A <Provider /> component provides the store, which it receives as a prop:

import store from './store/store'

const App = () => (
  <Provider store={store}>
    <ModalContainer />
  </Provider>
)

export default App

<Provider /> makes the store available to child components using the React Context API:

// react-redux/Provider.js

import React, { Component } from 'React'
import Context from './Context'

export default class Provider extends Component {
  constructor(props) {
    super(props)
    this.state = {
      storeState: props.store.getState()
    }
  }

  // ..

  render() {
    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
  }
}

Note: Read the React docs if you aren't familiar with the context API

A child component can access the <Provider /> component's state, which includes the storeState value, by rendering a Context.Consumer:

const ModalStatus = () => (
  <Context.Consumer>
    {({ storeState }) => <p>Modal is: {storeState.modal.visible}</p>}
  </Context.Consumer>
)

With this implementation the storeState value is static. To make sure the storeState is up to date, the <Provider /> component needs to re-render when the store state updates.

When a <Provider /> component mounts, it subscribes to the store using the store subscribe method. Remember subscribe adds a listener (a callback function) to the store listeners array. Each listener is called after the new store state is calculated during dispatch:

// redux/createStore.js

export default function createStore(reducer) {
  // ..
  let listeners = []

  // ..

  const subscribe = listener => {
    listeners.push(listener)
  }

  const dispatch = action => {
    // ..
    listeners.forEach(l => l())
  }

  // ..
}

The listener callback calls this.setState with the new store state:

// react-redux/Provider.js

export default class Provider extends Component {
  // ..
  componentDidMount() {
    const store = this.props.store

    store.subscribe(() => {
      this.setState({
        storeState: store.getState()
      })
    })
  }
  // ..
}

setState then triggers a component re-render, which in turn re-renders each of the <Provider /> child components (unless you code against it using componentShouldUpdate or some other method). So each of the child components of <Provider /> is re-rendered.

This is where connect comes in. Remember, connect creates a container component that maps the store state to the props of a wrapped component. Something like this:

const mapStateToProps = state => ({ visible: state.modal.visible })

export default connect(mapStateToProps)(Modal)

connect returns a <Connect /> component that renders its wrapped component. <Connect /> then generates props for the wrapped component by calling mapStateToProps with the store state available in the <Context.Consumer /> child function.

An inefficient implementation would look like this:

import React from 'react'
import Context from './Context'

export default function connectHOC(mapStateToProps) {
  return function wrapWithConnect(WrappedComponent) {
    const Connect = () => {
      const renderWrappedComponent = ({ storeState }) => {
        const props = mapStateToProps(storeState)
        return <WrappedComponent {...props} />
      }
      return <Context.Consumer>{renderWrappedComponent}</Context.Consumer>
    }

    return Connect
  }
}

With this implementation the <Connect /> component re-renders each time the store updates. In a large app this could be a big performance hit.

Redux improves on this simple implementation by using a caching technique known as memoization.

Memoization

Memoization (pronounced mem-oh-i-zay-shun) is a fancy word for caching the results of a function call.

Memoization is used to avoid recomputing expensive function calls, like rendering a React component tree.

One implementation of memoization works by saving the result and arguments of the last function call. If the next function call has the same argument as the previous call, the cached result is returned. If the argument is different, the function computes and caches a new result:

const memoizeFn = fn => {
  let prevResult
  let prevArg
  return arg => {
    if (prevArg === arg) {
      return prevResult
    }
    const result = fn(arg)
    prevArg = arg
    prevResult = result
    return result
  }
}

const calculateResult = a => a * 2

const memoizedFn = memoizeFn(calculateResult)
memoizedFn(1) // calls `calculateResult`
memoizedFn(1) // returns cached arg
memoizedFn(1) // returns cached arg
memoizedFn(2) // calls `calculateResult`

Memoization only works for functions when the calculation function is deterministic. That is, given the same input it always returns the same result. For example, a sum function that returns the sum of its arguments is deterministic:

const sum = (a, b) => a + b

An example of a non-deterministic function is a timeAgo function that uses Date.now to calculate the current time. The function would return a different result each time it's called:

const timeAgo = t => Date.now() - t

Another requirement for memoization is that the function must be free from side effects. Side effects are changes to an application state outside of the function. For example, a setTitle function that sets the document.title has the side effect of changing the value of document.title:

const setTitle = title => {
  document.title = title
}

A deterministic function that doesn't produce any side-effects is known as a pure function. Pure functions are vital for memoization to work, which is why redux tutorials are so strict about keeping your reducers pure.

Most memoization implementations use equality checks to ensure arguments haven't changed. So to grok memoization in Redux, you need to understand how JavaScript compares values.

JavaScript values

As of 2020, there are seven primitive JavaScript types: Undefined, Null, Boolean, Number, BigInt, Symbol, and String.

When you compare two values using the strict equality operator (===), the JS engine produces either true or false. The way the JS engine determines equality depends on the type of the values being compared.

Two different strings are said to equal each other if they have the exact same sequence of code units:

const str = 'some string'
str === str // true
str === 'some string' // true
str === 'different string' // false

Two numbers are said to equal each other if they contain the same number value:

const num = 1
num === num // 1
num === 1 // true
num === 2 // false

The other data type in JavaScript is Object. Everything that isn't a primitive type in JavaScript has type Object, including functions (callable objects) and arrays.

An Object has a unique object value that is separate from its shape:

const obj1 = { prop1: true } // has unique object value
const obj2 = { prop1: true } // has unique object value

When you compare two Object types using the strict equality operator (===), the JS engine compares the object values, rather than the shapes:

obj1 === obj1 // true
obj2 === obj2 // true
obj1 === obj2 // false

Note: Symbols work in a similar way to Objects during equality checks.

Now that you understand memoization and JavaScript values, you can see how redux uses it to avoid unnecessary re-renders.

Optimizing the Connect component

By now you have a good overview of Redux, but the devil is in the details. The complexity of the Redux source code comes from the optimizations used to avoid re-rendering.

By the end of this section you'll understand how the optimizations work, which will make it easier to keep your own code optimized.

Remember, in react-redux the connect function creates a <Connect /> component that renders a wrapped component using props derived from mapStateToProps.

The naive implementation renders a new component each time <Connect /> updates:

import React from 'react'
import Context from './Context'

export default function connectHOC(mapStateToProps) {
  return function wrapWithConnect(WrappedComponent) {
    const Connect = () => {
      const renderWrappedComponent = ({ storeState }) => {
        let props = mapStateToProps(storeState)
        return <WrappedComponent {...props} />
      }
      return <Context.Consumer>{renderWrappedComponent}</Context.Consumer>
    }

    return Connect
  }
}

With the naive implementation the wrapped component (and all its child components) will re-render each time dispatch is called. Not good.

The solution is to stop the wrapped component from re-rendering if the props object created by mapStateToProps hasn't changed since the last render.

Redux can't use a strict equality (===) check to make sure the new props are the same as the previous props, because mapStateToProps returns a new object (with a new object value) each time it's called.

Instead, redux uses a shallowEqual helper function. shallowEqual loops through each property and asserts that it strict equals the previous property:

// react-redux/shallow-equal.js

export default function shallowEqual(objA, objB) {
  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  for (let i = 0; i < keysA.length; i++) {
    if (objA[keysA[i]] !== objB[keysA[i]]) {
      return false
    }
  }

  return true
}

So when a <Connect /> component re-renders, it can call mapStateToProps with the new state, and use shallowEqual to compare the previous props with the newly generated props:

const nextProps = mapStateToProps(state)
const propsChanged = !shallowEqual(lastDerivedProps, nextProps)

To perform this calculation, Redux needs a reference to the previous props. Redux uses memoization to do this:

function makeDerivedPropsSelector(mapStateToProps) {
  let lastDerivedProps

  return function selectDerivedProps(state) {
    lastState = state

    const nextProps = mapStateToProps(state)
    const propsChanged = !shallowEqual(lastDerivedProps, nextProps)

    if (propsChanged) {
      lastDerivedProps = nextProps
    }

    return lastDerivedProps
  }
}

The selectDerivedProps selector is created in <Connect />'s constructor function:

Note: A selector is a function that uses memoization. You see this terminology a lot in the redux ecosystem.

export default function connect(mapStateToProps) {
  return function wrapWithConnect(WrappedComponent) {
    class Connect extends PureComponent {
      constructor(props) {
        super(props)
        this.selectDerivedProps = makeDerivedPropsSelector(mapStateToProps)
      }
      // ..
    }
    return Connect
  }
}

selectDerivedProps returns the previous props object if the previous props shallow equal the new props:

function makeDerivedPropsSelector(mapStateToProps) {
  let lastDerivedProps

  return function selectDerivedProps(state) {
    // ..

    if (propsChanged) {
      lastDerivedProps = nextProps
    }

    return lastDerivedProps
  }
}

So <Connect /> can use the strict equality operator to check if the props changed:

let derivedProps = this.selectDerivedProps(storeState)

if (derivedProps !== previousProps) {
  // do something
}

If the component props have changed, <Connect /> should update the wrapped component. If the props have not changed, <Connect /> should not update the component.

One way to avoid re-rendering a child in React is to return the same element that was used in the previous render.

<Connect /> uses memoization again to return the previously generated rendered element if the component does not need to update. This is done with another selector, selectChildElement. If the props have changed, the selector will re-render <WrapperComponent />:

function makeChildElementSelector(WrappedComponent) {
  let lastChildProps
  let lastChildElement

  return function selectChildElement(childProps) {
    if (childProps !== lastChildProps) {
      lastChildProps = childProps
      lastChildElement = <WrappedComponent {...childProps} />
    }
    return lastChildElement
  }
}

So now the full <Connect/> component using selectors will calculate new props using the makeDerivedProps selector. It then calls selectChildElement with the new props. If the props have not changed, the selector will return the previously rendered element, and the wrapped component will not re-render. In full it looks like this:

// react-redux/connect.js

export default function connectHOC(mapStateToProps) {
  return function wrapWithConnect(WrappedComponent) {
    class Connect extends PureComponent {
      constructor(props) {
        super(props)
        this.selectDerivedProps = makeDerivedPropsSelector(mapStateToProps)
        this.selectChildElement = makeChildElementSelector(WrappedComponent)
        this.renderWrappedComponent = this.renderWrappedComponent.bind(this)
      }

      renderWrappedComponent({ storeState }) {
        let derivedProps = this.selectDerivedProps(storeState)
        return this.selectChildElement(derivedProps)
      }

      render() {
        return (
          <Context.Consumer>{this.renderWrappedComponent}</Context.Consumer>
        )
      }
    }
    return Connect
  }
}

Note: The Simple Redux source code includes extra checks to avoid re-renders. Check out the code

In the original react-redux code, the makeDerivedProps selector uses mapStateToProps, mapDispatchToProps, mergeProps, and the own props of the <Connect /> component to compute the props. But it works on the same principle: if the result of generating the new component props shallow equals the previous props, then the previous props object is returned. Which means the selectChildElement function can return the previously rendered component if childProps === lastChildProps.

Optimizing your containers

The main takeaway from this is that Redux re-renders based on strict equality checks.

If your mapStateToProps function returns a new object each time it's computed, the shallow check will fail and the component will re-render each time dispatch is called:

const mapStateToProps = state => ({
  childProps: {
    name: state.modal.name
  }
})

Instead, use primitive values in mapStateToProps:

const mapStateToProps = state => ({
  childName: state.modal.name
})

Alternatively, you can use a selector library, like Reselect, to create memoized functions that always return the same object if the values it depends on have not changed:

const getName = state => state.modal.name

const childPropsSelector = createSelector(
  [getName],
  name => ({ name })
)
const mapStateToProps = state => ({ childProps: childPropsSelector(state) })

Congratulations, you've reached the end of the Simple Redux walk-through. At this point you should have a good understanding of how Redux works under-the-hood.

Now go forth and avoid unnecessary re-renders in your app!

Further reading

More Repositories

1

avoriaz

๐Ÿ”ฌ a Vue.js testing utility library
JavaScript
761
star
2

vue-hackernews

A Vue hacker news application with tests
JavaScript
157
star
3

vue-test-loader

Extract custom test blocks from Vue components
JavaScript
131
star
4

jest-transform-stub

Jest stub transform
JavaScript
113
star
5

vue-test-utils-jest-example

An example vue-test-utils project with jest
JavaScript
91
star
6

vue-unit-test-perf-comparison

Comparison of Vue SFC unit tests using different test runners
JavaScript
64
star
7

parcel-vuejs-template

Parcel template for Vue CLI
JavaScript
63
star
8

jest-serializer-vue

Jest Serializer for Vue components
JavaScript
62
star
9

notes

Notes on computer science https://notes.eddyerburgh.me/ ๐Ÿ“š
TeX
59
star
10

vue-test-utils-vuex-example

Example repository testing vuex with vue-test-utils
JavaScript
52
star
11

testing-vuex-store-example

Testing a Vuex store
JavaScript
48
star
12

example-front-end-test-pyramid-app

An example app demonstrating the front-end pyramid
JavaScript
41
star
13

vue-digital-clock

๐Ÿ•š Simple digital clock built with Vue.js
JavaScript
41
star
14

vue-test-utils-ava-example

An example vue-test-utils project with AVA
JavaScript
30
star
15

mock-vuex-in-vue-unit-tests-tutorial

Example repo demonstrating how to mock Vuex in Vue unit tests
JavaScript
29
star
16

vue-test-utils-karma-example

JavaScript
26
star
17

data-structures-in-practice

Learn about data structures and how they are used in open source projects. https://data-structures-in-practice.com/
SCSS
22
star
18

react-boilerplate

๐Ÿš€ Quickly start a react project with this boilerplate
JavaScript
18
star
19

how-to-unit-test-vue-components-for-beginners

Repo for tutorial on how to test vue components
JavaScript
16
star
20

avoriaz-ava-example

Testing Vue components with AVA and avoriaz example
JavaScript
16
star
21

git-init-plus

โšก Turbo charged git init
Shell
15
star
22

avoriaz-karma-mocha-example

Example using avoriaz with karma and mocha to test Vue.js components
JavaScript
13
star
23

avoriaz-jest-example

Example project using avoriaz and jest
JavaScript
12
star
24

vue-email-signup-form-app

Demonstration repo
JavaScript
12
star
25

avoriaz-mocha-example

Example using avoriaz with mocha-webpack to test Vue.js components
JavaScript
12
star
26

testing-a-vuex-store-example-app

Example app for testing Vuex
JavaScript
10
star
27

vue-test-utils-tape-example

An example vue-test-utils project with tape
JavaScript
8
star
28

tape-vue-example

JavaScript
8
star
29

vue-jest-example

Example project using Jest
JavaScript
7
star
30

vue-test-loader-example

An example project using vue-test-loader to extract tests
Vue
7
star
31

react-dev-tools-iframe-webpack-plugin

Inject code to set up React dev tools for code running in an iframe
JavaScript
6
star
32

vue-unit-test-starter

Starter project for Vue unit testing tutorials
JavaScript
6
star
33

dom-event-types

DOM event data scraped from MDN
TypeScript
5
star
34

vue-test-utils-mocha-example

An example vue-test-utils project with mocha
JavaScript
5
star
35

conditional-specs

Conditional spec methods for test suites that run in multiple environments
JavaScript
4
star
36

palette-generator

๐ŸŽจ Webapp that generates color palettes. Built with Vue + Vuex
JavaScript
4
star
37

vue-add-globals

Add globals to a Vue instance ๐ŸŒŽ
JavaScript
4
star
38

vue-amsterdam-example

Example from Vue Amsterdam talkโ€”slides.com/eddyerburgh/testing-vue-components
JavaScript
4
star
39

blog

๐Ÿ“• My personal blog
SCSS
3
star
40

vue-test-utils-proposal

Proposed API for vue test utils
3
star
41

stub-route-in-vue-unit-tests

A demonstration of how to stub $route in Vue unit tests
JavaScript
3
star
42

avoriaz-karma-jasmine-example

Simple avoriaz example using karma and jasmine
JavaScript
3
star
43

jest-vue-example

Finished project of Vue Jest
JavaScript
2
star
44

express-mongo-rest-api-boilerplate

An express REST API boilerplate with mongo and mongoose. Dockerized
JavaScript
2
star
45

stub-vue-components-inject-loader

JavaScript
1
star
46

codemods

JavaScript
1
star
47

is-dom-selector

JavaScript function to validate a DOM selector
JavaScript
1
star
48

vue-ios-calculator

Vue IOS calculator app
Vue
1
star
49

palette-picker

A web app that lets you store and copy swatches of color. Built with react + redux
JavaScript
1
star