Redux
The store
Middleware
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)
);
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
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(() => {
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
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>
)
}
}
...
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
- Action builders (unexported)
- Thunks
- handleAddGoal
- handleDeleteGoal
-
src/actions/shared.js
- Types
- Action builders (unexported)
- Thunks
- 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
- 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
- Getting access to a certain portion of the state
- Getting access to
dispatch
-
useSelector
-
useDispatch