Twitter khuong291
Twitter khuong291

Reputation: 11702

Reducer changed state but does not re render

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

Answers (1)

John Ruddell
John Ruddell

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

Related Questions