Skip to content

Instantly share code, notes, and snippets.

@PAkerstrand
Last active September 7, 2017 08:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save PAkerstrand/bf842d2fcdd22ddcef2239c693fa780c to your computer and use it in GitHub Desktop.
Save PAkerstrand/bf842d2fcdd22ddcef2239c693fa780c to your computer and use it in GitHub Desktop.
React Europe workshop

This gist contains some of the exercises and (my) solutions to we did during the workshop at React Europe

// Create a counter reducer and make the tests pass
// Bonus: Create a counters reducer that manage a
// collection of counters
const counter = () => {
// ?
}
expect(counter(0, { type: 'INCREMENT' })).toEqual(1);
expect(counter(1, { type: 'INCREMENT' })).toEqual(2);
expect(counter(2, { type: 'DECREMENT' })).toEqual(1);
expect(counter(undefined, {})).toEqual(0);
console.log('All tests pass');
// The state of the reducer defaults to 0 if we
// don't get passed a state or an undefined state
const counter = (state = 0, action) => {
// Switch on the type so we can handle the actions
// we are interested in
switch(action.type) {
case 'INCREMENT':
// If it's an INCREMENT action, then increment
// the value of the state by one
return state + 1;
case 'DECREMENT':
// If it's an DECREMENT action, then decrement
// the value of the state by one
return state - 1;
default:
// We got an action that we don't know how to
// handle, so return the state unaltered
return state;
}
}
// The counters reducer manages a collection of counters,
// so its default state is an empty array (i.e. it does not
// yet manage any counters).
const counters = (state = [], action) => {
switch(action.type) {
case 'ADD_COUNTER':
// When we are asked to add a counter, we add it to the
// back of the collection of counters. Note that we do
// NOT mutate the state and `push` to the existing state,
// rather we spread the existing counters, and add the
// new counter at the end of the list
return [...state, 0];
case 'INCREMENT':
case 'DECREMENT':
// If we get an INCREMENT or DECREMENT action, we look at
// the `index`-property of the action to determine which
// of the counters the action applies to. Remember, actions
// are only required to have a `type`-property. Other than
// that, we can use whatever properties and fields we want
// and need in our actions to achieve our goal.
// Also, since we already have a reducer that knows how to
// manage the state for a single counter, we delegate the
// responsibility of updating the state of the counter to
// that reducer.
return state.map((s, i) => i === action.index ? counter(s) : s)
default:
return state;
}
}
/* 1. Create a store from a reducer using `createStore`.
* 2. Dispatch an action from inside an click event listener using `dispatch`.
* (You can just attach the listener to `document`.)
* 3. Subscribe to state changes and render the current count by setting the `innerHTML` of the body.
*
* Bonus: reimplement `createStore`
*/
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const store = // ?
const root = document.getElementById('root');
// Re-implement `createStore`. (initial state and store enhancers omitted)
const createStore = (reducer) => {
// Keep track of our current state
let state;
// Keep track of the listeners subscribed to the store
let listeners = [];
// Allow users to query the store for the current state
const getState = () => state;
// Expose dispatch as the only way to update the state
// (Ignore the fact that the state can be mutated by
// calling `getState()` and mutate the returned state)
const dispatch = (action) => {
// Run the reducer for the current state with the
// provided action, and set the return value as the
// new state
state = reducer(state, action);
// Notify each subscriber that a state update has occured
listeners.forEach(l => l());
};
// Allow users to subscribe to state changes
const subscribe = (listener) => {
// Add the listener to the list of listeners. Note that we
// mutate our listeners array here. But since it is only
// used internally, it doesn't really matter.
listeners.push(listener);
// Return a function that allows the listener to unsubscribe
return () => {
listeners = listeners.filter(l => l !== listener);
};
}
// Dispatch an initial action to get the state initialized
dispatch({ type: '@@INIT_ACTION' });
// Expose the public API to consumers
return {
getState,
dispatch,
subscribe,
};
}
// Usually, we'd `import { createStore } from 'redux'`, but since
// JSFiddle has Redux loaded as a UMD-module, it is exposed as the
// global `Redux`.
const { createStore } = Redux;
// 1. Create the store using our `counter`-reducer
const store = createStore(counter);
// 2. In our click handler we just dispatch an `INCREMENT`-action
const clickHandler = () => store.dispatch({
type: 'INCREMENT',
});
// Setup a listener to call our click handler whenever the document
// is clicked
document.addEventListener('click', clickHandler);
// Get a reference to the root element of our app (In JSFiddle this
// was supplied in the HTML-tab as a div with id="root"
const root = document.getElementById('root');
// A naïve render function that just sets the innerHTML of our root
// element to the current state of our store.
const render = () => root.innerHTML = store.getState();
// 3. Subscribe to the store, so each change of our state gets rendered
store.subscribe(render);
// Perform an initial render to get the app started
render();
/* 1. Remove the click listener on the document
* 2. Create a <Counter>-component that can render a counter.
* 2.1. The counter should have buttons for increasing and
* decreasing the value of the counter
* 2.2. Clicking on the buttons should dispatch a corresponding
* action to the store
* 2. Render the counter using your <Counter>-component
* 3. Subscribe to the store and re-render the <Counter>-component
* when the store's state changes
*
* Bonus:
* - Create a component that manages a collection of counters
* - Add a button for adding new counters
* - Each counter can be updated individually
*/
const { createStore } = Redux;
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const store = createStore(counter);
const root = document.getElementById('root');
document.addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT' });
});
const render = () => {
root.innerHTML = store.getState().toString();
}
store.subscribe(render);
render();
const store = createStore(counters);
// The custom `<Button>`-component is not strictly necessary,
// but by having a custom component for the button we make it
// much easier to customize and theme buttons throughout our
// React application
const Button = (props) => (
<button {...props}>{props.children}</button>
);
// The `<Counter>`-component hasn't changed. A counter still
// works as before (OK, it has changed. It now renders the buttons
// using our custom `<Button>`-component. But functionality-wise
// it is identical to the previous `<Counter>`)
const Counter = ({ value, onIncrement, onDecrement }) => (
<div>
<h1>{value}</h1>
<Button onClick={onIncrement}>+</Button>
<Button onClick={onDecrement}>-</Button>
</div>
);
// Our new `<Counters>`-component take an array of counters and callbacks
// for when the user interacts with a counter or requests a new counter.
// When the user interacts with a counter we now call `onIncrement` and
// `onDecrement` with the index of the counter that the user interacted
// with. This allows us to pass this index on in our actions, which is
// then used by the reducer to update the state of the correct counter
const Counters = ({ counters, onIncrement, onDecrement, addCounter }) => (
<div>
<Button onClick={addCounter}>Add counter</Button>
{counters.map((counter, i) => (
<Counter
key={i}
value={counter}
onIncrement={() => onIncrement(i)}
onDecrement={() => onDecrement(i)}
/>
))}
</div>
);
// Not much has changed in the `render`-function. We now create
// an extra callback that dispatches the `ADD_COUNTER`-action
// when the user clicks the "Add counter"-button. Additionally,
// we pass the index provided by as an argument to `onIncrement`
// and `onDecrement` in our actions.
const render = () => {
ReactDOM.render(
<Counters counters={store.getState()}
onIncrement={(index) => {
store.dispatch({ type: 'INCREMENT', index });
}}
onDecrement={(index) => {
store.dispatch({ type: 'DECREMENT', index });
}}
addCounter={() => {
store.dispatch({ type: 'ADD_COUNTER' });
}}
/>,
document.getElementById('root')
);
}
store.subscribe(render);
render();
// We use a stateless functional to create our Counter, since
// we do not need any state or lifecycle hooks.
// As props we take the value of the counter, as well as
// callback for when the increment or decrement buttons are
// clicked.
const Counter = ({ value, onIncrement, onDecrement }) => (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>
+
</button>
<button onClick={onDecrement}>
-
</button>
</div>
);
// Now just replace the contents of our `render`-function to
// utilize ReactDOM. For `onIncrement` and `onDecrement` we
// pass arrow functions that dispatch actions to our store
const render = () => {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => {
store.dispatch({ type: 'INCREMENT' });
}}
onDecrement={() => {
store.dispatch({ type: 'DECREMENT' });
}}
/>,
root
);
}
// We need to still subscribe to the store to ensure that our
// UI reflects the state of our app
store.subscribe(render);
// Finally, we still call `render` to get something rendered into
// our document
render();
(function() {
'use strict';
/* Write a todo list reducer
* ADD_ACTION adds a todo with `id`, `text`, and `completed`
* TOGGLE_TODO toggles a todo's `completed` field
* Don't mutate the state!
* Use tests to verify.
*/
const todos = (state = [], action) => {
// ?
};
/* Tests */
const testAddTodo = () => {
const stateBefore = [];
const action = {
type: 'ADD_TODO',
id: 0,
text: 'Learn Redux'
};
const stateAfter = [
{
id: 0,
text: 'Learn Redux',
completed: false
}
];
deepFreeze(stateBefore);
deepFreeze(action);
expect(todos(stateBefore, action)).toEqual(stateAfter);
};
const testToggleTodo = () => {
const stateBefore = [
{
id: 0,
text: 'Learn Redux',
completed: false
},
{
id: 1,
text: 'Go shopping',
completed: false
}
];
const action = {
type: 'TOGGLE_TODO',
id: 1
};
const stateAfter = [
{
id: 0,
text: 'Learn Redux',
completed: false
},
{
id: 1,
text: 'Go shopping',
completed: true
}
];
deepFreeze(stateBefore);
deepFreeze(action);
expect(todos(stateBefore, action)).toEqual(stateAfter);
};
testAddTodo();
testToggleTodo();
console.log('All tests passed.');
})();
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
// Return a new array with the contents of the previous state,
// and with the new todo item added last
return [
...state,
{
id: action.id,
text: action.text,
completed: false,
}
];
case 'TOGGLE_TODO':
// `Array.map` ensures we create a new array
return state.map((todo) => {
// If the id of the todo item is not affected, just
// return the todo item unaltered. As long as we never
// mutate objects, sharing references is great! We save
// memory and work by not having to create a copy of the
// todo item in the new state
if (todo.id !== action.id) {
return todo;
}
// However, if this IS the affected todo item, we need to
// shallowly copy it in order to preserve the todo item in
// the previous state. Remember, NO mutations!
// In the new object we overwrite the `completed`-field by
// negating the previous value.
return {
...todo,
completed: !todo.completed,
};
});
default:
return state;
}
};
/**
* Split the `todo` reducer out of the `todos` reducer.
*
* The `todo` reducer would contain the logic for how an individual todo
* is added and updated, and the `todos` reducer would contain the logic
* for adding and mapping items in the array.
*
* Make sure the tests still pass after the refactoring.
*/
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo => {
if (todo.id !== action.id) {
return todo;
}
return {
...todo,
completed: !todo.completed
};
});
default:
return state;
}
};
const testAddTodo = () => {
const stateBefore = [];
const action = {
type: 'ADD_TODO',
id: 0,
text: 'Learn Redux'
};
const stateAfter = [
{
id: 0,
text: 'Learn Redux',
completed: false
}
];
deepFreeze(stateBefore);
deepFreeze(action);
expect(todos(stateBefore, action)).toEqual(stateAfter);
};
const testToggleTodo = () => {
const stateBefore = [
{
id: 0,
text: 'Learn Redux',
completed: false
},
{
id: 1,
text: 'Go shopping',
completed: false
}
];
const action = {
type: 'TOGGLE_TODO',
id: 1
};
const stateAfter = [
{
id: 0,
text: 'Learn Redux',
completed: false
},
{
id: 1,
text: 'Go shopping',
completed: true
}
];
deepFreeze(stateBefore);
deepFreeze(action);
expect(todos(stateBefore, action)).toEqual(stateAfter);
};
testAddTodo();
testToggleTodo();
console.log('All tests passed.');
/*
* The todo reducer manages a single todo item. This means that
* we can't really have a default state. In order to create the
* default state we need to handle the "ADD_TODO" action. Thus,
* our default state is `undefined` and we do not declare a
* default value for the state.
*/
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
// In `ADD_TODO` we can set up our default state.
// We just return a new todo item with the values
// provided in the action, as well as the `completed`
// field set to false
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
// Check if this action applies to the todo item
// we are managing. If it does not, return the current
// state unaltered.
if (state.id !== action.id) {
return state;
}
// This action applies to the todo item managed by our
// reducer, so create a copy of the state and overwrite
// the `completed`-field by inverting it.
return {
...state,
completed: !state.completed
};
default:
// This is an unknown action for the todo item we're
// managing, so return the todo item unaltered.
return state;
}
};
/*
* After extracting the logic for how to handle a single todo item
* into its own reducer, we can simply delegate to that reducer for
* each todo item. Thus, the only code that remains in the
* todos-reducer is how to manage the collection of todo items.
*/
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, todo(undefined, action)];
case 'TOGGLE_TODO':
return state.map(t => todo(t, action));
default:
return state;
}
};
/* Create a `visibilityFilter` reducer that holds
* the current filter. It's just a string.
*
* Using `combineReducers`, create a root reducer
* `todoApp` that composes the `todo` reducer and
* the `visibilityFilter` reducer.
*
* Optional: implement the root reducer manually.
*
* Optional: implement `combineReducers`.
*/
const todo = // Identical to solition for exercise 5
const todos = // Identical to solution for exercise 5
const { createStore, combineReducers } = Redux;
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
// ?
};
const todoApp = // ?
const store = createStore(todoApp);
const testAddTodo = () => {
const stateBefore = {
todos: [],
visibilityFilter: 'SHOW_ALL'
};
const action = {
type: 'ADD_TODO',
id: 0,
text: 'Learn Redux'
};
const stateAfter = {
todos: [{
id: 0,
text: 'Learn Redux',
completed: false
}],
visibilityFilter: 'SHOW_ALL'
};
deepFreeze(stateBefore);
deepFreeze(action);
expect(todoApp(stateBefore, action)).toEqual(stateAfter);
};
const testToggleTodo = () => {
const stateBefore = {
todos: [{
id: 0,
text: 'Learn Redux',
completed: false
}, {
id: 1,
text: 'Go shopping',
completed: false
}],
visibilityFilter: 'SHOW_ALL'
};
const action = {
type: 'TOGGLE_TODO',
id: 1
};
const stateAfter = {
todos: [{
id: 0,
text: 'Learn Redux',
completed: false
}, {
id: 1,
text: 'Go shopping',
completed: true
}],
visibilityFilter: 'SHOW_ALL'
};
deepFreeze(stateBefore);
deepFreeze(action);
expect(todoApp(stateBefore, action)).toEqual(stateAfter);
};
const testSetVisibilityFilter = () => {
const stateBefore = {
todos: [],
visibilityFilter: 'SHOW_ALL'
};
const action = {
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
};
const stateAfter = {
todos: [],
visibilityFilter: 'SHOW_COMPLETED'
};
deepFreeze(stateBefore);
deepFreeze(action);
expect(todoApp(stateBefore, action)).toEqual(stateAfter);
};
testAddTodo();
testToggleTodo();
testSetVisibilityFilter();
console.log('All tests passed.');
/* combineReducers take an object where each key becomes the
* key in the resulting state and each entry is the reducer
* that manage that specific slice of the state.
*/
function combineReducers(reducerMap) {
// Since we're paranoid, and want to ensure that we do not
// change the shape of our state if `reducerMap` would be
// mutated outside this function, we create a copy of the
// keys and reducers at the time the function is called
const keys = Object.keys(reducerMap);
const reducers = Object.values(reducerMap);
// Return a new reducer
return (state = {}, action) => {
// For each action we get, we create a fresh new state
const nextState = {};
// Then we iterate over the shape of our state, the keys
keys.forEach(key => {
// For each slice of the state, retrieve the reducer for
// that slice
const reducer = reducers[key];
// Then find the current state for that slice
const stateManagedByReducer = state[key];
// Finally, delegate to our reducer to create the next
// state for this slice.
nextState[key] = reducer(stateManagedByReducer, action);
});
// We're done, so return the new state!
return nextState;
}
}
const rootReducer = (state = {}, action) => ({
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
})
/* The visibility filter reducer is very simple. We handle
* only a single action, and the state we manage is a string.
* Strings are immutable in JS, so we can simply return the
* filter provided to us in the filter action and be certain
* that the reference won't be modified by some other code.
*
* As always, unknown actions returns the state unaltered.
*/
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};
// `combineReducers` is exposed by Redux as
// a top level named export.
const { combineReducers } = Redux;
// Compose our `todos` and `visibilityFilter`
// reducers by passing them to combineReducers
const todoApp = combineReducers({
todos,
visibilityFilter
});
/*
* We want to implement the following components:
* - `FilterLink` displays a link for a visibility filter.
* - `Footer` displays three `FilterLink`s.
* - `Todo` displays a single todo.
* - `TodoList` displays a list of todos.
* - `AddTodo` displays an input with a button.
* - `TodoApp` renders `AddTodo`, `TodoList`, and a `Footer`, and
* passes functions that dispatch actions as callback props to them.
* The component structure and their props are outlined below,
* but you will write the implementation!
*/
const { Component } = React;
const FilterLink = ({
filter,
currentFilter,
children,
onClick
}) => {
// If currentFilter is the same as filter,
// return a span. Otherwise return a link
// that calls onClick when it's clicked.
// ?
};
const Footer = ({
visibilityFilter,
onFilterClick
}) => (
// Render a FilterLink for each
// of the filters (all, active, completed).
// ?
);
const Todo = ({
onClick,
completed,
text
}) => (
// Render a single todo.
// ?
);
const TodoList = ({
todos,
onTodoClick
}) => (
// Render an array of todos.
// ?
);
const AddTodo = ({
onAddClick
}) => {
// Render an input field with a button.
// return ?
};
const getVisibleTodos = (
todos,
filter
) => {
// Return filtered todos.
// You probably want to `switch` and `return`
// inside `case`s depending on the `filter`.
}
let nextTodoId = 0;
const TodoApp = ({
todos,
visibilityFilter
}) => (
// Render the form, a list of todos, and a footer.
// Pass callback props to dispatch Redux actions!
// ?
);
const render = () => {
ReactDOM.render(
<TodoApp
{...store.getState()}
/>,
document.getElementById('root')
);
};
store.subscribe(render);
render();
const FilterLink = ({
filter,
currentFilter,
children,
onClick
}) => {
// If currentFilter is the same as filter,
// return a span. Otherwise return a link
// that calls onClick when it's clicked.
if (filter === currentFilter) {
return (
<span>{children}</span>
);
} else {
return (
<a href="#" onClick={(e) => { e.preventDefault(); onClick();}}>
{children}
</a>
);
}
};
// Render a FilterLink for each
// of the filters (all, active, completed).
const Footer = ({
visibilityFilter,
onFilterClick
}) => (
<p>
Show:
{' '}
<FilterLink
filter='SHOW_ALL'
currentFilter={visibilityFilter}
onClick={() => onFilterClick('SHOW_ALL')}
>
All
</FilterLink>
{', '}
<FilterLink
filter='SHOW_ACTIVE'
currentFilter={visibilityFilter}
onClick={() => onFilterClick('SHOW_ACTIVE')}
>
Active
</FilterLink>
{', '}
<FilterLink
filter='SHOW_COMPLETED'
currentFilter={visibilityFilter}
onClick={() => onFilterClick('SHOW_COMPLETED')}
>
Completed
</FilterLink>
</p>
);
// Render a single todo.
const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
);
// Render an array of todos.
const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{
todos.map(todo => (
<Todo
key={todo.id}
{...todo}
onClick={ () => onTodoClick(todo.id) }
/>
))
}
</ul>
);
// Render an input field with a button.
const AddTodo = ({
onAddClick
}) => {
let node;
return (
<div>
<input ref={inputNode =>
node = inputNode}
/>
<button
onClick={() => {
onAddClick(node.value);
node.value = '';
}}
>
Add
</button>
</div>
);
};
const getVisibleTodos = (
todos,
filter
) => {
// Return filtered todos.
// You probably want to `switch` and `return`
// inside `case`s depending on the `filter`.
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
throw new Error(`Bad filter value "${filter}"`);
}
}
// Render the form, a list of todos, and a footer.
// Pass callback props to dispatch Redux actions!
let nextTodoId = 0;
const TodoApp = ({
todos,
visibilityFilter,
onAddClick,
onTodoClick,
onFilterClick
}) => (
<div>
<AddTodo
onAddClick={onAddClick}
/>
<TodoList
todos={getVisibleTodos(todos, visibilityFilter)}
onTodoClick={onTodoClick}
/>
<Footer
visibilityFilter={visibilityFilter}
onFilterClick={onFilterClick}
/>
</div>
);
/* Now we will introduct React Redux's `connect()`
*
* Rather than using a single subscription at the root of the app,
* we will use multiple subscriptions, using container components.
*
* - Divide FilterLink into Link and a connected FilterLink.
* - Create VisibleTodoList by connecting TodoList
* - Create ConnectedAddTodo by connecting AddTodo and use
* `dispatch` in the click handler to add new todo items
* - Remove the top level subscription to the store
*/
// [...] Code identical to the solution of Exercise 7.
/**
* We can also connect without any arguments. This type
* of connected component gets `dispatch` injected into
* its props, and we can then use it as we see fit.
* So in this version of `AddTodo` we utilize `dispatch`
* in the `onClick`-handler of the `<button>` we render
* to dispatch an action to the Redux store.
*/
let nextTodoId = 0;
const AddTodo = ({
dispatch
}) => {
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = '';
}}>
Add Todo
</button>
</div>
);
};
const ConnectedAddTodo = connect()(AddTodo);
/* Utility function to ensure we show the right todo items.
* This is actually an example of a selector function. If we
* would change the `todos` argument into `state, and co-locate
* it with the `todos`-reducer, it would be a perfect fit!
* However, we are still just on day 1 of the workshop, so we
* won't do that for now.
*/
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}
/* Our TodoList does not need to change in this exercise. It
* still takes the todo items to show and a callback that is
* invoked whenever one of the todo items is clicked.
*
* Also note the syntax when we map over the todo items and
* render each todo item. We spread over the `todo`-object and
* pass them as props to `<Todo>`. But since it does not contain
* a `onClick`-prop, we create it explicitly and pass it down as
* well.
*/
const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);
/* In our `mapStateToProps`-function, all the information that we
* need to create the `todos`-prop for `<TodoList>` is in the
* Redux-store. Thus we can omit the `ownProps`-argument.
* Additionally, we use destructuring to directly get at the
* `todos` and `visibilityFilter` of the state.
*
*/
const mapStateToTodoListProps = ({ todos, visibilityFilter }) => {
return {
todos: getVisibleTodos(todos, visibilityFilter)
};
};
/* In our `mapDispatchToProps`-argument, we make use of the
* second option for that argument. If we pass an object, then all
* functions in that object is assumed to be action creators and
* they are automatically wrapped in `dispatch`. Like before, the
* keys of the object corresponds to the name of the property we
* want injected. In our case, `<TodoList>` wants an `onTodoClick`-
* property, so that's what we name the key.
*/
const mapDispatchToTodoListProps = {
onTodoClick: (id) => ({
type: 'TOGGLE_TODO',
id
})
};
/* Connect using the mapping functions above, and save the
* resulting component in VisibleTodoList.
*/
const VisibleTodoList = connect(
mapStateToTodoListProps,
mapDispatchToTodoListProps
)(TodoList);
/* Since `FilterLink` now can decide whether it is active
* or not we only need to pass `filter` as a prop. This
* is what our updated `Footer` looks like:
*/
const Footer = () => (
<p>
Show:
{' '}
<FilterLink filter='SHOW_ALL'>All</FilterLink>
{', '}
<FilterLink filter='SHOW_ACTIVE'>Active</FilterLink>
{', '}
<FilterLink filter='SHOW_COMPLETED'>Completed</FilterLink>
</p>
);
/* We update our `TodoApp` to use the connected versions
* of the components. Since all the child components are
* now connected where appropriate, we no longer need to
* take any props.
*/
const TodoApp = () => (
<div>
<ConnectedAddTodo />
<VisibleTodoList />
<Footer />
</div>
);
/* We wrap our `TodoApp` in a `Provider` and pass
* the store instance to it
*/
ReactDOM.render(
<Provider store={store}>
<TodoApp />
</Provider>,
document.getElementById('root')
);
// We no longer need a top level subscription to
// the store. State updates are handled automatically
// by the connected components! Awesome!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment