状态更新后反应不重新渲染

React not re-rendering after state update

我目前正在学习 React。在我的主页组件中,我使用一个钩子来初始化和填充状态。我有 2 个状态,1 个包含随机口袋妖怪,另一个是 20 个随机口袋妖怪的数组。第一个工作正常,但不是数组。

这是主页组件:

// Hook
import { useHomeFetch } from "../hooks/useHomeFetch";
// Image
import NoImage from '../images/missingno.png';

const Home = () => {
    const { state, collection, loading, error } = useHomeFetch();
    return (
        <>
            { state.heroPokemon ?
            <HeroImage 
                image={`${API.fetchSprite(state.heroPokemon)}`}
                title={state.heroPokemon.name}
                text='Placeholder, should include abilities, etc. to show the pokemon off.'
            />
            : null}
            { collection.pokemons[0] ?
            <Grid header='Popular pokemons'>
                {collection.pokemons.map( pokemon => (
                    <h3 key={pokemon.id}>{pokemon.name}</h3>
                ) )}
            </Grid>
            : null}
        </>
    );
}

export default Home;

添加条件渲染后英雄宝可梦运行良好。我的第一个想法是对集合做同样的事情,因为它可能在所有 API 调用承诺都已解决之前呈现。

如果我查看 React 开发工具,这就是我看到的状态:

hooks
HomeFetch

1 State : {heroPokemon: {…}}
2 State : {pokemons: Array(20)}
           pokemons :
               [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]

所以在我看来,调用确实完成得很好,里面有 20 个随机的 pokemon 对象。

这里是钩子:

const initialState = {
    heroPokemon: null
}

const fetchRandomPokemons = async (ids) => {
    let pokemons = [];
    ids.forEach(async num => {
        pokemons.push(await API.fetchPokemon(num));
    });
    return pokemons;
}

export const useHomeFetch = () => {
    const [state, setState] = useState(initialState);
    const [collection, setCollection] = useState({pokemons: []});
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(false);
    
    const fetchPokemons = async (limit = 20) => {
        try {
            setError(false);
            setLoading(true);
            
            const heroPokemon = await API.fetchPokemon(randomInt(1, 898));
            const randomInts = randomUniqueInt(1, 898, limit);
            const pokemons = await fetchRandomPokemons(randomInts);

            setState(prev => ({heroPokemon}));
            setCollection(prev => ({pokemons}));
        } catch (error) {
            setError(true);
        }
        setLoading(false);
        
    };
    // Initial render
    useEffect(() => {
        fetchPokemons(20);
    }, [])

    return { state, collection, loading, error };
};

当我去掉条件时,Grid 出现了,但它没有带有口袋妖怪名称的 h3。如果我 console.log 出 statecollection 就在 home 的 return 之前。最后几个显示正确的 state.heroPokemoncollection.pokemons 正确填充。这使我相信状态已正确更新,但为什么 React 不重新渲染网格组件?

补充一下,我也试过这样补充:

<p>{collection.pokemons[0]}</p>

到主页组件,没有任何显示。我觉得我可能误解或遗漏了状态和重新渲染如何工作的关键部分,这导致我遗漏了我可能做错的地方。

编辑: 这是 API 调用 return 的内容:pokeapi。co/api/v2/pokemon/ditto 我还调试了 Home,这就是它的样子:imgur.com/a/jVhaSsk

我现在很困惑,因为 pokemon.name 显然存在并且不是未定义的。为什么它们没有出现在 h3 中?

编辑 2: 我将主页组件更改为:

<Grid header='Popular pokemons'>
{
    state.results.map(pokemon => {
        console.log(pokemon.name)
        return <h3>pokemon name</h3>
     })
}
</Grid>

看起来 h3 仍然是空的。还在等待数组中的promise吗?但是 console.log 可以很好地打印出口袋妖怪的名字。

编辑 3: 感谢德鲁,我找到了问题所在。 这是我的 Grid 组件的样子:

import React from "react";

import { Wrapper, Content } from './Grid.styles';

const Grid = ({ header }) => (
    <Wrapper>
        <h1>{header}</h1>
    </Wrapper>
);

export default Grid;

我也在使用 styled-components,这是文件:

import styled from 'styled-components';

export const Wrapper = styled.div`
    max-width: var(--maxWidth);
    margin: 0 auto;
    padding: 0 20px;

    h1 {
        color: var(--medGrey);

        @media screen and (max-width: 768px) {
            font-size: var(--fontBig);
        }
    }
`;

export const Content = styled.div`
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    grid-gap: 2rem;
`;

如您所见,Content 不在 Wrapper 中。我改成后:

const Grid = ({ header, children }) => (
    <Wrapper>
        <h1>{header}</h1>
        <Content>{children}</Content>
    </Wrapper>
);

效果很好。看起来 Pokemon 数组正在作为 {children} 传递给 Grid,所以当它不存在时它不会渲染它。

试试这个:

 // Hook
import { useHomeFetch } from "../hooks/useHomeFetch";
// Image
import NoImage from '../images/missingno.png';

const Home = () => {
    const [newCollection, setNewCollection] =useState([])
    const { state, collection, loading, error } = useHomeFetch();
    
    useEffect(()=>{
       setNewCollection(collection || []);
     },[collection])

 
    return (
        <>
            { state.heroPokemon ?
            <HeroImage 
                image={`${API.fetchSprite(state.heroPokemon)}`}
                title={state.heroPokemon.name}
                text='Placeholder, should include abilities, etc. to show the pokemon off.'
            />
            : null}
            { newCollection.pokemons[0] ?
            <Grid header='Popular pokemons'>
                {newCollection.pokemons.map( pokemon => (
                    <h3 key={pokemon.id}>{pokemon.name}</h3>
                ) )}
            </Grid>
            : null}
        </>
    );
}

export default Home;

如果仍然无法工作,请分享您的存储库。所以我可以帮助你更多。谢谢

问题

看来问题出在您的 fetchRandomPokemons 实用程序函数中。即使您已声明 fetchRandomPokemons async,您也不会等待任何事情。 Array.prototype.forEach 也是完全同步的。但是,您 确实 声明 .forEach 回调 async 并且 await 声明 API 响应,但是因为 .forEach 是同步的fetchRandomPokemons 函数已经 return 编辑了空 pokemons 数组并解析。 async关键字只允许一个函数作用域使用await关键字。

const fetchRandomPokemons = async (ids) => {
  let pokemons = [];
  ids.forEach(async num => { // <-- synchronous
    pokemons.push(await API.fetchPokemon(num));
  });
  return pokemons; // <-- returns []
}

...

const pokemons = await fetchRandomPokemons(randomInts); // <-- []

setCollection({ pokemons }); // <-- { pokemons: [] }

您记录的是 pokemons 数组,当每个 API.fetchPokemon Promise 解决时,.forEach 数组发生变异。

解决方案

ids 映射到一个 Promise 数组并等待它们全部解析,然后 return 解析的 pokemon 数组。

const fetchRandomPokemons = (ids) => {
  return Promise.all(ids.map(API.fetchPokemon));
}

...

const fetchPokemons = async (limit = 20) => {
  setError(false);
  setLoading(true);

  try {
    const heroPokemon = await API.fetchPokemon(randomInt(1, 898));
    const randomInts = randomUniqueInt(1, 898, limit);
    const pokemons = await fetchRandomPokemons(randomInts);

    setState({ heroPokemon });
    setCollection({ pokemons });
  } catch (error) {
    setError(true);
  }
  setLoading(false);
};

更新

正在添加 运行 codesandbox 和 screencap。