PB138: React and State Management

presented by Lukas Grolig

What is state management?

  • Something that stores data for your components so you don‘t have to pass them from the parent.
  • It also handles actions that happen in your components.
  • Shares state across multiple components

What is Flux?

  • It is architure for building client-side websites. It is utiling unidirectional data flow between component. It‘s pattern implemented by state management frameworks.

action

store

dispatcher

view

Object describing what happened

Component routing data to stores (usually by registered callbacks)

Holds some part of state and maintains related logic

Different implementations of Flux

React Context

Context provides a way to pass data through the component tree without having to pass props down manually at every level

Creating context

const ThemeContext = React.createContext('light');

First create context and provide default value. We suggest to export context and import it in components later.​

Providing values in context

class App extends React.Component { 
    render() { 
        return ( 
            <ThemeContext.Provider value="dark"> 
                <Toolbar /> 
            </ThemeContext.Provider> 
        ); 
    } 
}

The provider will pass a value to all components in the tree below. No matter how deep.

Or pass state from any component to the top. 

 

Consuming values from context

class MyComponent extends React.Component { 
    render() { 
        return ( 
            <ThemeContext.Consumer> 
	        {(context) => {
                    <Toolbar theme={context} /> 
                }}
            </ThemeContext.Consumer> 
        ); 
    } 
}

Component consuming context is wrapped by it.

Or using hooks

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ 
    	background: theme.background, 
        color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

MobX

  • It is a simple, scalable, and battle-tested state management solution.
  • MobX uses an object-oriented approach to state management.
  • Realy easy to use.

Data flow in MobX is similar as in Flow

action

observer(component)

Store

reaction

calls

notifies

How to implement a store

Observable

import { makeObservable, observable, computed, action, flow } from "mobx"

class Doubler {
    value

    constructor(value) {
        makeObservable(this, {
            value: observable,
            double: computed,
            increment: action,
            fetch: flow
        })
        this.value = value
    }
}

makeObservable will provide notification mechanisms for your data to be consumed by the observer.

If you don't have subclasses or inherit from super try to use makeAutoObservable.

Computed property

class Doubler {
    value

    constructor(value) {
        // ...
    }

    get double() {
        return this.value * 2
    }
}

The computed value uses other observables to calculate some value on top of them.

Observer

const doubler = new Doubler(2);

const DoublerView = observer(({ doubler }) 
	=> <span>Double: {doubler.double}</span>)
    
<DoublerView doubler>

Observer enables a component to listen to store changes. The store is passed in props. 

 

Action

import { makeObservable, observable, action } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeObservable(this, {
            value: observable,
            increment: action
        })
    }

    increment() {
        // Intermediate states will not become visible to observers.
        this.value++
        this.value++
    }
}

If some function in the store modifies state, it is an action.

Autorun

const counter = observable({ count: 0 })

// Sets up the autorun and prints 0.
const disposer = autorun(() => {
    console.log(counter.count)
})

// Prints: 1
counter.count++

When something in the store changes than your reaction is called. Autorun is like reaction, but you don't specify on what props you listen.  

Integration of a store

import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"

const TimerContext = createContext<Timer>()

const TimerView = observer(() => {
    // Grab the timer from the context.
    const timer = useContext(TimerContext) // See the Timer definition above.
    return (
        <span>Seconds passed: {timer.secondsPassed}</span>
    )
})

ReactDOM.render(
    <TimerContext.Provider value={new Timer()}>
        <TimerView />
    </TimerContext.Provider>,
    document.body
)

Redux

Redux is a predictable state container for JavaScript apps. As the requirements for JavaScript single-page applications have become increasingly complicated, our code must manage more state than ever before.

This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server.

UI state is also increasing in complexity, as we need to manage active routes, selected tabs, spinners, pagination controls, and so on.

Reducer hooks

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

State is read-only

The only way to change the state is to emit an action, an object describing what happened.

store.dispatch({
	type: 'COMPLETE_TODO‘,
	index: 1
})

Changes are made with pure functions

To specify how the state tree is transformed by actions, you write pure reducers.

function todos(state = [], action) {
	switch (action.type) {
		case 'COMPLETE_TODO‘:
			return state.map((todo, index) => { ... })
		default:
			return state
}

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Reducers

Reducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'counter/incremented') {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1
    }
  }
  // otherwise return the existing state unchanged
  return state
}

Store

The Store is the object that brings everything together. The store has the following responsibilities:
Holds application state;
Allows access to state via selectors;
Allows the state to be updated via dispatch(action);
Registers listeners via subscribe(listener);
Handles unregistering of listeners via the function returned by subscribe(listener)

 

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

Selector

import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodos = state => state.todos

const TodoList = () => {
  const todos = useSelector(selectTodos)

  // since `todos` is an array, we can loop over it
  const renderedListItems = todos.map(todo => {
    return <TodoListItem key={todo.id} todo={todo} />
  })

  return <ul className="todo-list">{renderedListItems}</ul>
}

export default TodoList

Dispatch

import React, { useState } from 'react'
import { useDispatch } from 'react-redux'

const Header = () => {
  const [text, setText] = useState('')
  const dispatch = useDispatch()

  const handleChange = e => setText(e.target.value)

  const handleKeyDown = e => {
    const trimmedText = e.target.value.trim()
    // If the user pressed the Enter key:
    if (e.key === 'Enter' && trimmedText) {
      // Dispatch the "todo added" action with this text
      dispatch({ type: 'todos/todoAdded', payload: trimmedText })
      // And clear out the text input
      setText('')
    }
  }

  return (
    <input
      type="text"
      placeholder="What needs to be done?"
      autoFocus={true}
      value={text}
      onChange={handleChange}
      onKeyDown={handleKeyDown}
    />
  )
}

export default Header

Pass store using Context

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import store from './store'

ReactDOM.render(
  // Render a `<Provider>` around the entire `<App>`,
  // and pass the Redux store to as a prop
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

Recoil

New kid on the block

Root

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

Root

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

Atom

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

useRecoilState

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );

Selector

const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

Working with REST API

Using SWR

import useSWR from 'swr'

const fetcher = url => fetch(url).then(r => r.json());

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

Using SWR with boundaries

import { ErrorBoundary, Suspense } from 'react'
import useSWR from 'swr'

function Profile () {
  const { data } = useSWR('/api/user', fetcher, { suspense: true })
  return <div>hello, {data.name}</div>
}

function App () {
  return (
    <ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <Profile />
      </Suspense>
    </ErrorBoundary>
  )
}

Questions?

Ok, that's it for today.

React State Management

By Lukáš Grolig

React State Management

  • 447