Graduate Program KB

Redux

The store

  • Most bugs are caused by state mismanagement, state the app expected and state it got were out of sync
  • Increasing the predictability of state decreases the amount of bugs in the application
  • Put all state in a single location
    • State tree
    • Define clear rules for how to access and update
  • Benefits
    • Shared cache
    • Access state without moving up
    • Can reload other parts of the application without throwing away state
    • The rest of the ap is just receiving state and displaying UI based on it - pure functions
    • Server rendering
      • Requires splitting out the state of the application and markup
      • Easier to split state out if it's in one location
    • createStore, 4 parts
      1. The state
      2. Get the state
      3. Listen to state changes
      4. Update the state
        function createStore() {
          let state // local to function, need to create an interface to interact
          let listeners = []
    
          const getState = () => state
          
          const subscribe (listener) => {
            listeners.push(listener)
            return () => { // unsub function
              listeners = listeners.filter((l)=> l !== listener)
            }
          }
    
          return {
            getState,
            subscribe
          }
        }
    
    • Representing state transforms as actions
      • Create a collection of all possible events that can occur to update the state
      • Action is payload of information that sends an instruction of what type of transformation to make to your application state
        const action = {
          type: "LIKE_TWEET",
          id: 950788310443724800,
          uid: "tylermcginnis",
        };
        
    • Reducer function
      • Function that takes state and update it must be pure
        • Always return the same result with the same arguments
        • Only depend on args
        • Never produce side effects
      function todos(state=[], action) {
        if (action.type === 'ADD_TODO') {
          return state.concat([action.todo])
        }
        return state;
      }
      
      • In createStore
      function createStore(reducer) {
        ...
        const dispatch(action) => {
          state = reducer(state, action)
          listeners.forEach((listener) => listener())
        }
        ...
        return {... dispatch, ...}
      }
      
    • Updating an object
      • Use Object.assign to merge mapped object with new
      • Or spread
    • Combining reducers
      function app(state={}, action) {
        return {
          todos: todos(state.todos, action)
          goals: goals(state.goals, action)
        }
      }
      
      • Root reducer that splits the state into different parts and delegate to sub handlers
      • combineReducers
      const reducer = combineReducers({
        users,
        settings,
        tweets
      });
      - Keys are state properties, values are reducer functions passed that part of the state
      
    • Action creators
      • Not portable to hard code action object into dispatch call
      function addTodoAction(todo) {
        return {
          type: ADD_TODO,
          todo
        }
      }
      

Middleware

  • Customizing dispatch
function checkAndDispatch(store, action) {
  if (
    action.type === ADD_TODO &&
    action.todo.name.toLowerCase().indexOf('bitcoin') !== -1
  ) {
    return alert("Nope. That's a bad idea")
  }
  return store.dispatch(action);
}
  • Everywhere you have store.dispatch would need to call checkAndDispatch
  • Redux middleware
    • Between when an action is dispatched and when the reducer runs
    • Can use for error reporting, routing, logging
  • Uses currying, next is either the next middleware in the chain or dispatch at the end
  function checker(store) {
    return function (next) {
      return function(action) {
        if (
          action.type === ADD_TODO &&
          action.todo.name.toLowerCase().indexOf('bitcoin') !== -1
        ) {
          return alert("Nope. That's a bad idea")
        }
        return next(action);
      }
    }
  }
  const store = Redux.createStore(
    Redux.combineReducers({todos, goals}), 
    Redux.applyMiddleware(checker)
  );
  • ES6
const checker = (store) => (next) => (action) => (
  (action.type === ADD_TODO &&
   action.todo.name.toLowerCase().indexOf('bitcoin') !== -1
  ) 
  ? alert("Nope. That's a bad idea")
  : next(action);
)
const logger = (store) => (next) => (action) => {
  console.group(action.type)
  console.log('The action: ', action)
  const result = next(action);
  console.log('The new state: ', store.getState())
  console.groupEnd()
  return result;
}
Redux.applyMiddleware(checker, logger)

Redux with react

  • Can import babel and use in a script tag with
    <script type="text/babel"></script>
    
    function List(props) {
      return (
        <ul>
          props.items.map((item) => (
            <li key={item.id}>
              <span onClick={()=>props.toggle && props.toggle(item.id)}
                style={{textDecoration:item.complete ? 'line-through' : 'none'}}
              > 
                {item.name} 
              </span>
              <button onClick={() => props.remove(item)}>X</button>
            </li>
          ))
        </ul>
      )
    }
    class Todos extends React.component {
      addItem = (e) => {
        e.preventDefault()
        const name = this.input.value
        this.input.value = ''
        this.props.store.dispatch(addTodoAction({
          id: generateId(), name, complete: false
        }))
      }
      removeItem = () => {
        this.props.store.dispatch(removeTodoAction(todo.id))
      }
      toggleItem(id) {
        this.props.store.dispatch(toggleTodoAction(id))
    
      }
      render() {
        return (
          <div>
            <h1> Todo List </h1>
            <input
              type='text'
              placeholder='Add Todo'
              ref={(input)=>this.input = input}
            />
            <button onClick={addItem}>Add todo</button>
            <List 
              items={this.props.todos}
              remove={this.removeItem}
              toggle={this.toggleItem}
            />
          </div>
        )
      }
    }
    class App extends React.component {
      componentDidMount() {
        const {store} = this.props
        // Hacky anti-patten, but we don't have state on the App to set
        store.subscribe(() => this.forceUpdate())
    
      }
      render() {
        const {store} = this.props;
        const {todos, goals} = store.getState()
        return (
          <div><Todos todos={todos} store={store}/></div>
        )
      }
    }
    ReactDOM.render(<App store={store}/>, document.getElementByID("app"))
    

Asynchronous redux

  • Fetching initial data
  • Promise.all waits on an array of promises
    class App extends React.Component {
      componentDidMount() {
        const { store } = this.props;
        Promise.all([
          API.fetchTodos(),
          API.fetchGoals()
        ]).then(([todos, goals]) => {
          store.dispatch(receiveDataAction(todos, goals))
        })
        ...
      }
    }
    
    • Loading states
    • Can put state in the redux store
      function loading(state=true, action) {
        switch(action.type) {
          case RECEIVE_DATA:
            return false
          default:
            return state;
        }
      }
      ...Redux.combineReducers({..., loading})...
      class App extends React.Component {
        ...
        render() {
          const {todos, goals, loading} = store.getState()
          if (loading === true) {
            return <h3>Loading</h3>
          }
        }
      }
      
      
    • Updating the server
    class Todos extends React.Component {
      ...
      removeItem = (todo) => {
        return API.deleteTodo(todo.id)
        .then(() => {
          this.props.store.dispatch(removeTodoAction(todo.id))
        })
      }
    }
    
    • Delay between hitting delete and item being removed - waiting for db update
    • Optimistic updates, remove from state first, have to undo if it fails
    removeItem = (todo) => {
      this.props.store.dispatch(removeTodoAction(todo.id))
      return API.deleteTodo(todo.id)
        .catch(() => {
          // NOTE: should be inserted in the same spot in list, todos aren't currently sorted by anything
          this.props.store.dispatch(addTodoAction(todo))
          alert('An error occurred. Try again')
        })
    }
    
    • Custom thunk
      • Mixing ui logic with api logic
      • Repeated pattern with catch
      • what if we moved data fetching logic inside the action creator
        function handleDeleteTodo() {
          return (dispatch) => {
            dispatch(removeTodoAction(todo.id))
            return API.deleteTodo(todo.id)
              .catch(() => {
                dispatch(addTodoAction(todo))
                alert('An error occurred Try again')
              })
          }
        }
        
      • Reducer expects and object with an action
      • If we pass a function we want to run it with dispatch
      • Middleware allows us to hook into between dispatch and reducer execution
      • Write middleware to run the function
        const thunk = (store) => (next) => (action) => {
          if (typeof action === 'function') {
            return action(store.dispatch)
          }
          return next(action)
        }
        ... Redux.applyMiddleware(thunk, ...)
        ...
        removeItem = (todo) => {
          this.props.store.dispatch(handleDeleteTodo(todo))
        }
        
      • Three stages of an async request
        • Start of request
        • success
        • Fail
      • Need to account for anf inform the user about each
      • redux-thunk library
        ... Redux.applyMiddleware(ReduxThunk.default, checker, logger))
        
        • Can substitute for above thunk, very similar implementation
    • Other ways to handle async
      • Redux Promise
      • Redux Saga

React-redux

  • Downsides of react and redux
    • Currently passing the store down to everything that needs it - won't scale well
    • Forcing update in main component - re-render entire ui when anything changes
  • Using Context
    • Only using store to call dispatch
      const Context = React.createContext()
      class Provider extends React.Component {
        render() {
          return (
            <Context.Provider value={this.props.store}>
            {this.props.children}
            </Context.Provider>
          )
        }
      }
      ...
      // App wants store in componentDidMount, not just render
      class ConnectedApp extends React.Component {
        ...
        render() {
          return (
            <Context.Consumer>
            {(store) => (
              <App store={store}/>
            )}
            </Context.Consumer>
          )
        }
      }
      ...
      class ConnectedGoals extends React.Component {
        render() {
          return (
            <Context.Consumer>
            {(store) => {
              const {goals} = store.getState()
              return <Goals goals={goals} dispatch = store.dispatch/>
            }}
            </Context.Consumer>
          )
        }
      }
      ...
      ReactDOM.render(
        <Provider store={store}>
          <ConnectedApp/>
        </Provider>,
        ...
      )
    
  • Repeated ConnectedComponent pattern
    • HOC
    • Render any component, passing that component any data it needs from the store, not using context directly
    const ConnectedApp = connect((loading) => ({
        loading: state.loading
      }))(App)
    
    • Function given to connect returns an object which will be passed as props to the component
    • Will also pass dispatch
    • Connect will be responsible for subscribing - so we don't need to subscribe
    class App extends React.Component {
      componentDidMount() {
        const {dispatch} = this.props
        dispatch(handleInitialData())
      }
      render() {
        const {loading} = this.props
        if (loading === true) {
          return <h3>Loading</h3>
        }
        return (
          <div>
            <ConnectedTodos/>
            <ConnectedGoals/>
          </div>
        )
      }
    }
    
  • Implementing connect
    function connect(mapStateToProps) {
      return (component) => {
        class Receiver extends React.Component {
          componentDidMount() {
            const {subscribe} = this.props.store
            this.unsubscribe = subscribe(() => this.forceUpdate())
          }
          componentWillUnmount() {
            this.unsubscribe()
          }
          render() {
            const {dispatch, getState} = store
            const state = store.getState()
            const stateNeeded = mapStateToProps(state)
            return <Component {...stateNeeded} dispatch={dispatch}/>
          }
        }
        class ConnectedComponent extends React.Component {
          render() {
            return (
            <Context.Consumer>
              {(store) => <Receiver store={store}/> }
            </Context.Consumer>
            )
          }
        }
        return ConnectedComponent;
      }
    }
    
  • Using react-redux
    const ConnectedApp = ReactRedux.connect((loading) => ({
        loading: state.loading
      })), App)
    ...
    ReactDOM.render(
      <ReactRedux.Provider store={store}>
        <ConnectedApp/>
      </ReactRedux.Provider>,
      ...
    )
    

Redux Folder Structure

  • create-react-app
  • Actions
    • src/actions/todos.js
      • Types
        • ADD_TODO
        • REMOVE_TODO
        • TOGGLE_TODO
      • Action builders (unexported)
        • addTodo
        • removeTodo
        • toggleTodo
      • Thunks
        • handleDeleteTodo
        • handleAddTodo
        • handleToggle
    • src/actions/goals.js
      • Types
        • ADD_GOAL
        • REMOVE_GOAL
      • Action builders (unexported)
        • addGoal
        • removeGoal
      • Thunks
        • handleAddGoal
        • handleDeleteGoal
    • src/actions/shared.js
      • Types
        • RECEIVE_DATA
      • Action builders (unexported)
        • receiveData
      • Thunks
        • handleInitialData
  • Reducers
    • src/reducers/goals.js
    • src/reducers/todos.js
    • src/reducers/loading.js
    • src/reducers/index.js
      • Root reducer (combineReducers)
  • Middleware
    • middleware/checker.js
    • middleware/logger.js
    • middleware/index.js
      • export default applyMiddleware(thunk, checker, logger)
  • Components
    • components/list.js
    • components/todos.js
    • components/goals.js
    • components/app.js
  • The store
    • src/index.js
      const store = createStore(reducer, middleware)
      ReactDOM.render(...)
      
  • Other patterns
    • Ducks
      • module file which represents a single reducer with any action creators and constants it uses
    • Domain based folders
      • Group your actions, reducers, etc... by domain instead of generic actions, reducers folders

Redux hooks

  • Connect is a HOC
  • Felt like it composed against react rather than with it
  • Could break logic out into custom hooks
  • 2 primary uses
    1. Getting access to a certain portion of the state
    2. Getting access to dispatch
  • useSelector
    • Extract data from Redux store using a selector function
      function Profile() {
        const user = useSelector((state) => state.user)
        ...
      }
      
    • Caveat: selecting multiple values
    • Uses strict reference equality to decide when to re-render
      • Returning a new object - always a new reference
    • Use a different selector for each piece of state
    • React will batch and only do 1 re-render
  • useDispatch
    • Returns a reference to the store's dispatch function
      function Profile {
        const dispatch = useDispatch()
        ...
      }