/**
* Combine Reducers
* Creates a team of sub-reducers
*
*/
function combineReducers(reducers) {
const rootReducer = (state = {}, action) => {
const newState = {};
for(let key in reducers) {
const subState = state[key];
const reducerFn = reducers[key];
newState[key] = reducerFn(subState, action);
}
return newState;
}
return rootReducer;
}
/**
* Store Library - it should have four parts
* 1. The state (state-tree)
* 2. Get the state (getState)
* 3. Listen to changes on the state (subscribe)
* 4. Update the state (dispatch)
*
*/
function createStore(reducer) {
let state;
let listeners = [];
const getState = () => state;
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((l) => l !== listener);
};
}
const dispatch = (action) => {
// Call root reducer
state = reducer(state, action);
// Call subscribers
listeners.forEach((listener) => listener());
}
dispatch({});
return {
getState,
subscribe,
dispatch,
};
}
/**
* connect:
* Render any component, passing that component any data it needs, from the store.
*/
const Context = React.createContext();
class Provider extends React.Component {
render() {
return (
<Context.Provider value={this.props.store}>
{this.props.children}
</Context.Provider>
);
}
}
function connect(mapStateToProps) {
return (BaseComponent) => {
class Receiver extends React.Component {
componentDidMount() {
const { subscribe } = this.props.store;
this.unsubscribe = subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { dispatch, getState } = this.props.store;
const state = getState();
const stateNeeded = mapStateToProps(state);
return (
<BaseComponent {...stateNeeded} dispatch={dispatch} />
);
}
}
class ConnectedComponent extends React.Component {
render() {
return (
<Context.Consumer>
{(store) => <Receiver store={store} />}
</Context.Consumer>
);
}
}
return ConnectedComponent;
}
}
/**
* Actions
*/
const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const addTodoAction = (newTodo) => {
return {
type: ADD_TODO,
payload: newTodo
};
}
const removeTodoAction = (id) => {
return {
type: REMOVE_TODO,
payload: id
};
}
const toggleTodoAction = (id) => {
return {
type: TOGGLE_TODO,
payload: id
};
}
/**
* Reducer (Pure Function)
*/
function todoReducer(state = [], action) {
switch(action.type) {
case ADD_TODO :
return state.concat([action.payload]);
case REMOVE_TODO :
return state.filter((todo) => todo.id !== action.payload);
case TOGGLE_TODO :
return state.map((todo) => {
return (todo.id !== action.payload)
? todo
: Object.assign({}, todo, { complete: !todo.complete });
});
default :
return state;
}
}
/**
* Middlewares
*/
const logger = (store) => (next) => (action) => {
const result = next(action);
console.group(action.type);
console.log('The action: ', action);
console.log('The new state: ', store.getState());
console.groupEnd();
return result;
}
// Thunk
// Encapsulate async logic into the action creators
// The action creators have to return a function that accepts dispatch as its argument
const thunk = (store) => (next) => (action) => {
if (typeof action === 'function') {
return action(store.dispatch);
}
return next(action);
}
/**
* Store
*/
const rootReducer = Redux.combineReducers({
todos: todoReducer
});
const store = Redux.createStore(rootReducer, Redux.applyMiddleware(logger, thunk));
/**
* Components
*/
function List({items, removeItem, toggleItem}) {
return (
<ul>
{
items.map((item) => (
<li key={item.id}>
<span
style={{
marginRight: '10px',
textDecoration: item.complete ? 'line-through' : 'none'
}}
onClick={() => toggleItem && toggleItem(item.id)}>
{item.name}
</span>
<button onClick={() => removeItem(item.id)}>X</button>
</li>
))
}
</ul>
);
}
class Todos extends React.Component {
addItem = (e) => {
e.preventDefault();
const name = this.input.value;
this.input.value = '';
this.props.dispatch(addTodoAction({
id: generateId(),
name,
complete: false
}));
}
removeItem = (id) => {
this.props.dispatch(removeTodoAction(id));
}
toggleItem = (id) => {
this.props.dispatch(toggleTodoAction(id));
}
render() {
const { todos } = this.props;
return (
<div>
<h1>Todos List</h1>
<input
type='text'
placeholder='Add a todo'
ref={(input) => this.input = input}
/>
<button onClick={this.addItem}>Submit</button>
<List
items={todos}
removeItem={this.removeItem}
toggleItem={this.toggleItem} />
</div>
);
}
}
const mapStateToProps = (state) => ({
todos: state.todos
});
const ConnectedTodos = connect(mapStateToProps)(Todos);
/**
* Render Components
*/
class App extends React.Component {
render() {
return (
<React.Fragment>
<ConnectedTodos />
</React.Fragment>
);
}
}
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);