Reputation: 11702
Here is my reducer:
export interface RootState {
todos: ToDo[];
}
const initialState = {
todos: []
};
export const todo = (
state: RootState = initialState,
action: Action
): RootState => {
switch (action.type) {
case TODO_ADD:
return {
todos: [...state.todos, action.payload.todo]
};
case TODO_TOGGLE_COMPLETE:
let todoArr = [...state.todos];
todoArr.forEach((todo: ToDo, index: number) => {
if (todo.id === action.payload.id) {
todoArr[index].isComplete = !todo.isComplete;
}
});
return {
todos: todoArr
};
default:
return state;
}
};
action:
export const TODO_ADD = "TODO_ADD";
export const TODO_TOGGLE_COMPLETE = "TODO_TOGGLE_COMPLETE";
export interface Action {
type: string;
payload: any;
}
export interface ToDo {
id: string;
todo: string;
isComplete: boolean;
}
const add = (todo: ToDo): Action => ({
type: TODO_ADD,
payload: { todo }
});
export const toggleComplete = (id: string): Action => ({
type: TODO_TOGGLE_COMPLETE,
payload: { id }
});
export const addToDo = (todo: ToDo) => (dispatch: any) => {
dispatch(add(todo));
};
and my component to call toggle complete:
import * as React from "react";
import { Button, Row, Col } from "antd";
import styled from "styled-components";
import { ToDo, toggleComplete } from "src/actions/todo";
import { connect } from "react-redux";
const mapDispatchToProps = (dispatch: any) => ({
toggleComplete: (id: string) => dispatch(toggleComplete(id))
});
type MapDispatchToProps = ReturnType<typeof mapDispatchToProps>;
type Props = MapDispatchToProps & {
todo: ToDo;
};
class TodoItem extends React.Component<Props> {
render() {
const { todo } = this.props;
return (
<Container>
<Col span={20}>
<h3>{todo.todo}</h3>
</Col>
<Col span={4}>
<Button
type="primary"
onClick={() => this.props.toggleComplete(this.props.todo.id)}
>
{todo.isComplete ? "Completed" : "Complete"}
</Button>
</Col>
</Container>
);
}
}
const Container = styled(Row)`
width: 100%;
`;
export default connect<undefined, MapDispatchToProps>(
undefined,
mapDispatchToProps
)(TodoItem);
todo
is getting from component list
import * as React from "react";
import { List } from "antd";
import styled from "styled-components";
import TodoItem from "./TodoItem";
import { connect } from "react-redux";
import { RootState } from "src/reducers/todo";
import { ToDo } from "src/actions/todo";
const mapStateToProps = (state: RootState) => ({
todos: state.todos
});
type StateProps = ReturnType<typeof mapStateToProps>;
class TodoList extends React.Component<StateProps> {
render() {
const { todos } = this.props;
return (
<Container>
<List
header={
<div>
<h2>Todo List</h2>
<h4>
There are {todos.length} {todos.length === 1 ? "todo" : "todos"}
</h4>
</div>
}
bordered
dataSource={todos}
renderItem={(todo: ToDo) => (
<List.Item>
<TodoItem todo={todo} />
</List.Item>
)}
/>
</Container>
);
}
}
const Container = styled.div`
width: 100%;
margin-top: 30px;
`;
export default connect(mapStateToProps)(TodoList);
I already implemented subscribe here:
store.subscribe(() => console.log(store.getState()));
and see the state changed, but component not re render.
Upvotes: 1
Views: 1334
Reputation: 25862
The issue is how you are mutating state in your reducer. Change line 24 in your reducer file from this
todoArr[index].isComplete = !todo.isComplete;
to this
todoArr[index] = {...todo, isComplete: !todo.isComplete};
essentially what you were trying to do is mutate a state object directly instead of creating a new object signature.
Another issue you are going to have is currently you're creating new todos with the same id. So all items are marked as completed when you complete one. Instead of this, you can just use the current timestamp as a unique id.
Change this
const todo: ToDo = {
id: "1",
todo: value,
isComplete: false
};
this.props.addToDo(todo);
to this
const todo: ToDo = {
id: `${new Date().valueOf()}`,
todo: value,
isComplete: false
};
this.props.addToDo(todo);
Upvotes: 5