React,Redux:导航失败后加载数据

React, Redux: Loading data after navigate fails

现在我是 react/redux 的新手,我想弄清楚为什么我不能遍历一堆数据。该应用程序非常简单:

我正在通过 Link 切换到 table 条目的页面 -> 数据已加载并显示...目前一切正常。但是当创建一个新条目并使用 useNavigate 切换回上一页时,我收到以下错误:

Uncaught TypeError: books.books.map is not a function

booksSlice.js:

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from 'axios';

const BASE_URL = 'http://127.0.0.1:8000/api/book/';
const initialState = {
    books: [],
    status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
    error: null
};

export const getBooks = createAsyncThunk('api/book', async (id=null, { rejectWithValue }) => {
    try {
        const response = await axios.get(BASE_URL);
        return response.data.results;
    } catch (err) {
        console.log(err);
        return rejectWithValue(err.response.data);
    }
});

export const postBook = createAsyncThunk('api/book', async(book, { rejectWithValue }) => {
    try {
        const response = await axios.post(BASE_URL, book);
        return response.data;
    } catch (err) {
        console.log(err);
        return rejectWithValue(err.response.data);
    }
});

const booksSlice = createSlice({
    name: 'books',
    initialState,
    extraReducers(builder) {
        builder
            .addCase(getBooks.pending, (state, action) => {
                state.status = 'loading';
            })
            .addCase(getBooks.fulfilled, (state, action) => {
                state.books = action.payload;
                state.status = 'succeeded';
            })
            .addCase(getBooks.rejected, (state, action) => {
                state.status = 'failed';
                state.error = action.error.message;
            })
    }
});

export const selectAllBooks = (state) => state.books;
export default booksSlice.reducer;

booksList.js:

import * as React from 'react';
import { Link } from 'react-router-dom';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getBooks, selectAllBooks } from './booksSlice';

const BooksList = () => {

    const dispatch = useDispatch();
    const books = useSelector(selectAllBooks);
    
    useEffect(() => {
        dispatch(getBooks());
    }, [dispatch]);

    return(
        <>
            <Stack sx={{ maxWidth: "80%", mx: "auto", mt: "2rem" }} spacing={2} direction="row">
                <Button variant="contained">
                    <Link className="link-create-book" to="create">Create Book</Link>
                </Button>
            </Stack>
            <TableContainer sx={{ maxWidth: "80%", mx: "auto", mt: "2rem" }} component={Paper}>
                <Table aria-label="simple table">
                    <TableHead>
                        <TableRow>
                            <TableCell>Title</TableCell>
                            <TableCell align="right">Description</TableCell>
                            <TableCell align="right">Author</TableCell>
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        { books.books && books.books.map(item => (
                            <TableRow
                                key={item.id}
                                sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
                            >
                                <TableCell component="th" scope="row">{item.title}</TableCell>
                                <TableCell align="right">{item.description.slice(0, 30)}...</TableCell>
                                <TableCell align="right">{item.full_author_name}</TableCell>
                            </TableRow>
                        )) }
                    </TableBody>
                </Table>
           </TableContainer>
        </>
    )
};
export default BooksList;

booksCreate.js:

import { useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from "react-router-dom";
import { postBook } from './booksSlice';
import { Link } from 'react-router-dom';

const BooksCreate = () => {

    const dispatch = useDispatch();
    const navigate = useNavigate();

    const [title, setTitle] = useState('')
    const [content, setContent] = useState('')
    const [author, setAuthor] = useState('');

    const onTitleChanged = (e) => setTitle(e.target.value);
    const onContentChanged = (e) => setContent(e.target.value);
    const onAuthorChanged = (e) => setAuthor(e.target.value);

    const onSavePostClicked = () => {
        try {
            dispatch(postBook({                    
                title,
                description: content,
                author,
            })).unwrap();

            setTitle('');
            setContent('');
            setAuthor('');
            navigate('/book');
        } catch (err) {
            console.log('Failed to post new book!', err);
        }
    };

    return(
        <section className="create-book-section">
            <h2>Add a new book</h2>
            <form>
                <label htmlFor="postTitle">Title:</label>
                <input type="text" id="postTitle" name="postTitle" value={title} onChange={onTitleChanged} />
                <label htmlFor="postDescription">Description:</label>
                <textarea id="postDescription" name="postDescription" value={content} onChange={onContentChanged} />
            <label htmlFor="postAuthor">Author:</label>
            <input type="text" id="postAuthor" name="postAuthor" value={author} onChange={onAuthorChanged} />
            <Stack sx={{ my: "1rem" }} spacing={2} direction="row">
                <Button variant="contained" onClick={onSavePostClicked} disabled={!canSave} >Create Book</Button>
                <Button variant="contained">
                    <Link className="link-create-book" to="/book">Back to books</Link>
                </Button>
            </Stack>
            </form>
        </section>
    )
};

export default BooksCreate;

除此之外,我还尝试通过控制台输出 response.data,在显示错误消息后,我可以在控制台中看到新数据,尽管 none的数据显示在我的window里面。 如果您需要查看更多数据,请现在告诉我。

感谢您的帮助,祝您今天愉快!

在您的 booksSlice.js 中的 createSlice 中,添加一个将您的状态重置为空的操作 []

reducers: {
    clearBooks(state){
      state.books= [];
    }
}

export const { 
  clearBooks
} = booksSlice.actions;

然后在您的 booksList.js 调用此阳离子以在组件卸载时清除状态,您可以添加额外的 useEffect 或编辑现有的

import { getBooks, selectAllBooks, clearBooks } from './booksSlice';
useEffect(() => {
   dispatch(getBooks());
   return () => {
       dispatch(clearBooks())
   }
}, [dispatch]);

好吧...所以回过头来看,解决方案很明显:我在一个 extraReducer 中犯了一个错误 - 当 get-method 失败时 - 忘记包含 post-method 的减速器!

.addCase(getBooks.rejected, (state, action) => {
    state.books = [];
    state.status = 'failed';
    state.error = action.error.message;
})
.addCase(postBook.fulfilled, (state, action) => {
    state.books.unshift(action.payload);
    state.status = 'succeed';
})