Skip to content

Instantly share code, notes, and snippets.

@francisrstokes
Last active May 28, 2019 02:31
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save francisrstokes/5750b58c5799038cf03a55ecadca7d8c to your computer and use it in GitHub Desktop.
Save francisrstokes/5750b58c5799038cf03a55ecadca7d8c to your computer and use it in GitHub Desktop.
Redux without redux - sharing state and one way data flow using only standard react
import React from 'react';
import ReactDOM from 'react-dom';
import {StateDispatcher} from './StateDispatcher';
const initialState = {counter: 0};
const rootReducer = (state, action) => {
switch (action.type) {
case 'INC':
return {...state, counter: state.counter + 1};
case 'DEC':
return {...state, counter: state.counter - 1};
default:
return state;
}
};
ReactDOM.render(
<StateDispatcher state={initialState} reducer={rootReducer}>
{({state: {counter}, dispatch}) => {
return <div>
{counter}
<br/>
<button onClick={() => dispatch({type: 'INC'})}>+</button>
<button onClick={() => dispatch({type: 'DEC'})}>-</button>
</div>
}}
</StateDispatcher>,
document.getElementById('root')
);
// More complex example implementing the classic "todo" application
import React from 'react';
import ReactDOM from 'react-dom';
import {StateDispatcher} from './StateDispatcher';
// Utility function for creating combined reducers. Not necessary, but nice to have
const combineReducers = (reducers) => (state, action) => {
return Object.entries(reducers).reduce((newState, [reducerKey, reducer]) => {
return {
...newState,
[reducerKey]: reducer(state[reducerKey], action)
}
}, state);
};
const initialState = {
todos: [
{
title: 'Do something',
id: -1,
complete: false
}
],
filter: 'all',
newTodoText: ''
};
// Used to generate new ids for the todos
let nextId = 0;
/*
Reducers
*/
const todosReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO': {
return [
...state,
{
title: action.payload,
id: nextId++,
complete: false
}
];
}
case 'TOGGLE_TODO':{
const findTodo = todo => todo.id === action.payload;
const todo = state.find(findTodo);
return [
...state.filter(todo => !findTodo(todo)),
{
...todo,
complete: !todo.complete
}
];
}
default: return state;
}
}
const filterReducer = (state, action) => {
switch (action.type) {
case 'APPLY_FILTER': return action.payload
default: return state;
}
}
const newTodoTextReducer = (state, action) => {
switch (action.type) {
case 'SET_NEW_TODO_TEXT': return action.payload;
case 'ADD_TODO': return '';
default: return state;
}
};
// The final combined reducer
const rootReducer = combineReducers({
todos: todosReducer,
filter: filterReducer,
newTodoText: newTodoTextReducer
});
const applyFilter = (filter, todos) => todos.filter(todo => {
switch (filter) {
case 'completed':
return todo.complete;
case 'uncompleted':
return !todo.complete;
default:
return true;
}
});
ReactDOM.render(
<StateDispatcher state={initialState} reducer={rootReducer}>
{({state, dispatch}) => {
return <div>
<div>
<ul>
{applyFilter(state.filter, state.todos).map(todo =>
<li
key={todo.id}
onClick={() => dispatch({type: 'TOGGLE_TODO', payload: todo.id})}
style={{
textDecoration: todo.complete ? 'line-through' : 'none'
}}
>
{todo.title}
</li>
)}
</ul>
</div>
<div>
<button onClick={() => dispatch({type: 'APPLY_FILTER', payload: 'all'})}>All</button>
<button onClick={() => dispatch({type: 'APPLY_FILTER', payload: 'completed'})}>Completed</button>
<button onClick={() => dispatch({type: 'APPLY_FILTER', payload: 'uncompleted'})}>Uncompleted</button>
</div>
<div>
<input
value={state.newTodoText}
onChange={e => dispatch({type: 'SET_NEW_TODO_TEXT', payload: e.target.value })}
onKeyPress={e =>
(e.key === 'Enter' && state.newTodoText)
? dispatch({type: 'ADD_TODO', payload: state.newTodoText})
: null
}
/>
<button
onClick={() => state.newTodoText && dispatch({type: 'ADD_TODO', payload: state.newTodoText})}
>
Add Todo
</button>
</div>
</div>
}}
</StateDispatcher>,
document.getElementById('root')
);
import React from 'react';
export class StateDispatcher extends React.Component {
constructor(props) {
super(props);
this.state = props.state || {};
this._dispatch = this.dispatch.bind(this);
}
dispatch(action) {
this.setState(state => this.props.reducer(state, action));
}
getArgs() {
return {
state: this.state,
dispatch: this._dispatch
}
}
render() {
const {children} = this.props;
return children
? typeof children === 'function'
? children(this.getArgs())
: React.createElement(children)
: null;
}
}
@rafales
Copy link

rafales commented Jul 25, 2018

The whole point of having callback to setState is to use state passed as an argument. Otherwise you will run into race conditions. So dispatch should actually be:

dispatch(action) {
this.setState(state => this.props.reducer(state, action);
}

@francisrstokes
Copy link
Author

Absolutely true - I updated the code. Thanks @rafales.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment