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
- 485