Search code examples
reactjsreduxreact-routerredux-thunk

React state is getting overwritten when coming back from child component


I have a Home page that loads book cards (with book image, title,etc.) from a GET call (using axios), I am using a redux action caller to fire the API call "getAllBooks" on componentDidMount(). I am also using the react-router Link component to link the book with an "id", where once a user clicks on the book card, he will be taken to the BookView page where the id is taken from this.props.match.params. The BookView component loads the API call to get "bookById", using another action caller (which is in the same file of the previous action caller, and both share the same reducer).

The problem occurs when I go back to the Home page from the BookView page (either using the browser's back button or using this.props.history.goBack()). When returning to the HomeView, the state gets overwritten by the "BookByID" action, and I am unable to get the old state back (therefore I get the undefined error on the Home page). I tried putting the action in different files (of course this is useless because I am using the same reducer for both the actions). I tried setting componentDidUpdate on the Home page to fire the action when the props don't match. Tried dispatching an action to the redux state (of that reducer) to reset it, nothing works. I am not sure what mistake I am making, please be kind enough to direct me with a solution. I am using React 16.8, with Redux 7.1 (and Thunk to help with the async call) for this.

Home.js


import { getBooks, resetGetBooks } from "../../redux/actions/bookDbAction";
class Home extends Component {
  constructor(props) {
    super(props);

    this.signal = true;
    this.titleInput = React.createRef();
    this.descriptionInput = React.createRef();
    this.isbnInput = React.createRef();
    this.isbn13Input = React.createRef();
    this.grIdInput = React.createRef();
    this.imgLinkLargeInput = React.createRef();
    this.imgLinkMediumInput = React.createRef();
    this.imgLinkSmallInput = React.createRef();
  }

  componentDidMount() {
    this.props.getBooks();
  }

  // when component re-renders
  // componentDidUpdate(prevProps, prevState) {
  //   if(prevProps.books !== this.props.books) {
  //     this.props.getBooks();
  //   }
  // }

  renderBooks() {
    const { classes, books, loading, error } = this.props;
    if (loading) {
      return (
        <div className={classes.progressWrapper}>
          <CircularProgress />
        </div>
      );
    }

    if (books.length === 0) {
      return (
        <Typography className={classes.noResults} variant="h4">
          There are no books available
        </Typography>
      );
    }

    return (
      <Grid container spacing={3}>
        {books.map(book => (
          <Grid item key={book.id} lg={2} md={4} xs={6}>
            <Link className={classes.link} to={`/book/${book.id}`}>
              <BookCardGrid book={book} />
            </Link>
          </Grid>
        ))}
      </Grid>
    );
  }

  render() {
    const { classes } = this.props;

    return (
      <CoreLayout title="Home">
        <div className={classes.root}>
          <BookToolbar />
          <div className={classes.content}>{this.renderBooks()}</div>
          <div className={classes.pagination}>
            <Typography variant="caption">1-6 of 20</Typography>
            <IconButton>
              <ChevronLeftIcon />
            </IconButton>
            <IconButton>
              <ChevronRightIcon />
            </IconButton>
          </div>
        </div>
      </CoreLayout>
    );
  }
}

const mapStateToProps = state => {
  return {
    books: state.book.data,
    loading: state.book.dataLoading,
    error: state.book.error
  };
};

const mapDispatchToProps = {
  getBooks,
  resetGetBooks
};

Home.defaultProps = {
  books: [],
  loading: true,
  error: ""
};

Home.propTypes = {
  classes: PropTypes.object.isRequired,
  books: PropTypes.array.isRequired,
  loading: PropTypes.bool.isRequired,
  error: PropTypes.string.isRequired
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(withStyles(styles)(Home));

Book.js <--- This is the BookView page

import { getBookById, resetGetBookById } from "../../redux/actions/bookDbAction";


class Book extends Component {
  constructor(props) {
    super(props);

    this.signal = true;

    this.state = {
      isLoading: false,
      book: {
        bookTitle: "",
        description: "",
        isbn: "",
        isbn13: "",
        grId: "",
        imgLinkLarge: "",
        imgLinkMedium: "",
        imgLinkSmall: ""
      },
      error: null
    };
  }

  componentDidMount() {
    const { id } = this.props.match.params;
    this.props.getBookById(id);
  }

  componentWillUnmount() {
    this.props.resetGetBookById();
  }

  goBack = () => {
    this.props.history.goBack();
  };

  render() {
    const { classes, book, loading, error } = this.props;

    return (
      <CoreLayout title={book.title}>
        <div className={classes.root}>
          <IconButton
            className={classes.iconButton}
            onClick={this.goBack}
            size="medium"
          >
            <BackIcon fontSize="medium" />
          </IconButton>
          {loading ? (
            <div className={classes.progressWrapper}>
              <CircularProgress />
            </div>
          ) : (
            <div className={classes.content}>
              <div className={classes.imageWrapper + " image-wrap"}>
                <img
                  alt={book.title}
                  className={classes.image}
                  src={book.img_m}
                />
              </div>
            </div>
          )}
        </div>
      </CoreLayout>
    );
  }
}

Book.propTypes = {
  classes: PropTypes.object.isRequired
};

Book.defaultProps = {
  books: [],
  loading: true,
  error: ""
};

const mapStateToProps = state => {
  return {
    book: state.book.data,
    loading: state.book.dataLoading,
    error: state.book.error
  };
};

const mapDispatchToProps = {
  getBookById,
  resetGetBookById
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(withStyles(styles)(Book));

BookDbAction.js <--- This is the Book action file with all the action creators

import {
  GET_ALL_BOOKS_PENDING,
  GET_ALL_BOOKS_SUCCESS,
  GET_ALL_BOOKS_FAILURE,
  GET_ALL_BOOKS_RESET,
  GET_BOOK_BY_ID_PENDING,
  GET_BOOK_BY_ID_SUCCESS,
  GET_BOOK_BY_ID_FAILURE,
  GET_BOOK_BY_ID_RESET
} from "./types";

import axios from "axios";

const URL = `${process.env.REACT_APP_DEVELOPMENT_SERVER_URL}/book`;

export const getBooksPending = () => ({
  type: GET_ALL_BOOKS_PENDING,
  dataLoading: true
});

export const getBooksSuccess = json => ({
  type: GET_ALL_BOOKS_SUCCESS,
  dataLoading: false,
  payload: json
});

export const getBooksFailure = error => ({
  type: GET_ALL_BOOKS_FAILURE,
  dataLoading: false,
  payload: error
});

export const getBooksReset = () => ({
  type: GET_ALL_BOOKS_RESET
});

export const getBooks = () => {
  return async dispatch => {
    try {
      let response = await axios.get(URL);
      dispatch(getBooksPending());
      let data = await response.data;
      dispatch(getBooksSuccess(data));
    } catch (error) {
      console.log(error);
      dispatch(getBooksFailure(error));
    }
  };
};

export const resetGetBooks = () => {
  return dispatch => {
    dispatch(getBooksReset());
  };
};

export const getBookByIdPending = () => ({
  type: GET_BOOK_BY_ID_PENDING,
  dataLoading: true
});

export const getBookByIdSuccess = json => ({
  type: GET_BOOK_BY_ID_SUCCESS,
  dataLoading: false,
  payload: json
});

export const getBookByIdFailure = error => ({
  type: GET_BOOK_BY_ID_FAILURE,
  dataLoading: false,
  payload: error
});

export const getBookByIdReset = () => ({
  type: GET_BOOK_BY_ID_RESET
});

export const getBookById = id => {
  return async dispatch => {
    try {
      let response = await axios.get(`${URL}/${id}`);
      dispatch(getBookByIdPending());
      let json = await response.data;
      dispatch(getBookByIdSuccess(json));
    } catch (error) {
      dispatch(getBookByIdFailure(error));
    }
  };
};

export const resetGetBookById = () => {
  return dispatch => {
    dispatch(getBookByIdReset());
  };
};

export const addBookPending = () => ({
  type: ADD_BOOK_PENDING,
  dataLoading: true
});

export const addBookSuccess = data => ({
  type: ADD_BOOK_SUCCESS,
  dataLoading: false,
  payload: data
});

export const addBookFailure = error => ({
  type: ADD_BOOK_FAILURE,
  dataLoading: false,
  payload: error
});

export const addBookReset = () => ({
  type: ADD_BOOK_RESET
});

export const addBook = data => {
  return async dispatch => {
    try {
      let response = await axios.post(`${URL}`, {
        grid: data.grId,
        title: data.bookTitle,
        descr: data.description,
        isbn: data.isbn,
        isbn13: data.isbn13,
        img_l: data.imgLinkLarge,
        img_m: data.imgLinkMedium,
        img_s: data.imgLinkSmall
      });
      dispatch(addBookPending());
      let id = await response.data;
      const data = { id, ...data };
      // console.log("res:" + JSON.stringify(response));
      // console.log("id:" + id);
      dispatch(addBookSuccess(data));
    } catch (error) {
      dispatch(addBookFailure(error));
    }
  };
};

export const resetaddBook = () => {
  return dispatch => {
    dispatch(addBookReset());
  };
};

b>BookDbReducer.js <--- This is the Book reducerfile

import {
  GET_ALL_BOOKS_PENDING,
  GET_ALL_BOOKS_SUCCESS,
  GET_ALL_BOOKS_FAILURE,
  GET_BOOK_BY_ID_PENDING,
  GET_BOOK_BY_ID_SUCCESS,
  GET_BOOK_BY_ID_FAILURE,
  GET_BOOK_BY_ID_RESET
} from "../actions/types";

const initialState = {
  dataLoading: true,
  data: [],
  error: ""
};

const bookDbReducer = (state = initialState, action) => {
  switch (action.type) {
    case GET_ALL_BOOKS_PENDING:
      return {
        ...state,
        dataLoading: action.dataLoading
      };
    case GET_ALL_BOOKS_SUCCESS:
      return {
        ...state,
        dataLoading: action.dataLoading,
        data: action.payload
      };
    case GET_ALL_BOOKS_FAILURE:
      return {
        ...state,
        dataLoading: action.dataLoading,
        error: action.payload
      };
    case GET_BOOK_BY_ID_PENDING:
      return {
        ...state,
        dataLoading: action.dataLoading
      };
    case GET_BOOK_BY_ID_SUCCESS:
      return {
        ...state,
        dataLoading: action.dataLoading,
        data: action.payload
      };
    case GET_BOOK_BY_ID_FAILURE:
      return {
        ...state,
        dataLoading: action.dataLoading,
        error: action.payload
      };
    case GET_BOOK_BY_ID_RESET:
      return {
        ...state,
        dataLoading: false,
        data: null,
        error: null
      };
    default:
      return state;
  }
};

export default bookDbReducer;


Solution

  • You should try storing book detail data in another store property. This way the array of books will be preserved.

    const initialState = {
      dataLoading: true,
      data: [],
      bookDetails: {},
      error: ""
    };
    
    
    case GET_BOOK_BY_ID_SUCCESS:
      return {
        ...state,
        dataLoading: action.dataLoading,
        bookDetails: action.payload
      };