We have a working example of our app now, but our TodoApp
component has the input, button, list of todos, and filter links all inside of it.
We are going to refactor this single component into separate pieces so that they can be tested and worked on individually from one another.
class TodoApp extends Component {
render () {
const {
todos,
visibilityFilter
} = this.props;
const visibleTodos = getVisibleTodos(
todos,
visibilityFilter
);
return (
<div>
<input ref={node => {
this.input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
text: this.input.value,
id: nextTodoId++
});
this.input.value = '';
}}>
Add Todo
</button>
<ul>
{visibleTodos.map(todo =>
<li key={todo.id}
onClick={() => {
store.dispatch({
type: 'TOGGLE_TODO',
id: todo.id
});
}}
style={{
textDecoration:
todo.completed ?
'line-through' :
'none'
}}
>
{todo.text}
</li>
)}
</ul>
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
currentFilter={visibilityFilter}
>
All
</FilterLink>
{', '}
<FilterLink
filter='SHOW_ACTIVE'
currentFilter={visibilityFilter}
>
Active
</FilterLink>
{', '}
<FilterLink
filter='SHOW_COMPLETED'
currentFilter={visibilityFilter}
>
Completed
</FilterLink>
</p>
</div>
);
}
}
First, we will extract the Todo component that renders a single list item.
We will declare the Todo item as a function, which is available in React 14. We can remove the key
property, since it's only needed when we enumerate an array (we'll use it later when we have to enumerate many todos).
Previously we had hardcoded a click handler that dispatched 'TOGGLE_TODO'
. It's best practice with React to have several components that don't specify any behaviors, and only are concerned with how things are rendered (how they look). These are called presentational components.
Because we want our list to be a presentational component, we "promote" the onClick
handler to become a prop.
We also want to be more explicit about what the data is that the component needs to render. Instead of passing a todo
object, we will pass completed
and text
fields as separate props.
const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}
>
{text}
</li>
);
Now our Todo
component is purely presentational. It doesn't specify any behavior, but it knows how to render a single todo item.
The TodoList
component will accept an array of todos, and will render them into a <ul>
by using the todos.map()
function to render a Todo
component for each todo item.
We tell React to use each todo's id
as the unique key
for the elements, and we'll use the spread operator to send the todo
object's text
and completed
properties are sent as props to the Todo
component.
We need to specify what happens when a Todo
is clicked. Since we want to keep this as a presentational component, instead of dispatching an action, we'll specify a function onTodoClick()
and pass it todo.id
so it can decide what needs to happen. We will also pass onTodoClick
as a prop to the Todo
component.
const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
)
While presentational components just display data, we need a way to actually pass data from the store. This is where container components come in-- they can specify behavior and pass data.
In our example, TodoApp
is our container component.
Now that we've created TodoList
and Todo
presentational components, we can put them into our TodoApp
container component.
Our TodoApp
will render our TodoList
with visibleTodos
as the todos
, along with a callback that says when onTodoClick
is called with a todo id
, we should dispatch an action on the store of type 'TOGGLE_TODO'
along with the id
of the todo.
class TodoApp extends Component {
render () {
const {
todos,
visibilityFilter
} = this.props;
const visibleTodos = getVisibleTodos(
todos,
visibilityFilter
);
return (
<div>
<input ref={node => {
this.input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
text: this.input.value,
id: nextTodoId++
});
this.input.value = '';
}}>
Add Todo
</button>
<TodoList
todos={visibleTodos}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
} />
.
. // FilterLink stuff
.
</div>
);
}
}
The TodoApp
component renders a TodoList
and passes it a function that can dispatch an action.
The TodoList
component renders the Todo
component, and passes an onClick
prop which calls onTodoClick()
.
The Todo
component uses the onClick
prop it receives and binds it to the list item's onClick
. This way when it's called, the onTodoClick()
is called, which in turn dispatches the action, which in turn updates the visibile todos, since the action updates the store.