用数据初始化useState不会刷新页面

时间:2020-04-30 09:15:35

标签: javascript reactjs react-router frontend react-hooks

我有一条路线:

<Route path="/projects/:handle" component={Project} />

当我浏览到/projects/my-project-1时,页面成功加载,但是,如果我转到/projects/my-other-project,则页面内容保持不变(my-project-1内容)。

组件获取的match.params.handle发生了变化,但是useState仍然保留(my-project-1)数据。

const Project = ({ match }) => {

   const projectContext = useContext(ProjectContext)
   const [project] = useState(projectContext.getProject(match.params.handle))

   // my-other-project
   console.log(match.params.handle);

   let proj = projectContext.getProject(match.params.handle);

   // returns my-other-project
   console.log(proj)

   const [project] = useState(proj)

   // this is still my-project-1 data
   console.log(project);

   return (
       <>
        <Row>
            <Col sm={12} lg={12}>
                <ProjectOverview projectContext={projectContext} project={project} />
            </Col>
        </Row>
    </>
   );
}

export default Project;

加载组件数据的方式可能有什么问题,有没有更好的方法,也许通过useEffect? (我是有点新来反应(和反应钩子))

1 个答案:

答案 0 :(得分:2)

这不是您使用道具初始化状​​态的方式(我们将在一分钟内返回到状态)。

通常,没有理由将道具复制到状态。只需使用道具。这就是它的目的。它基本上是由父组件管理的组件的状态。所以:

const Project = ({ match }) => {

    const projectContext = useContext(ProjectContext)
    const project = projectContext.getProject(match.params.handle); // ***

    return (
        <>
            <Row>
                <Col sm={12} lg={12}>
                    <ProjectOverview projectContext={projectContext} project={project} />
                </Col>
            </Row>
        </>
    );
}

(附带说明:不需要该片段,您可以直接返回Row。)

如果projectContext.getProject(match.params.handle)是昂贵的操作,则可以通过useMemo进行记录:

const Project = ({ match }) => {

    const projectContext = useContext(ProjectContext)
    const project = useMemo(
        () => projectContext.getProject(match.params.handle),
        [match.params.handle]
    );

    return (
        <>
            <Row>
                <Col sm={12} lg={12}>
                    <ProjectOverview projectContext={projectContext} project={project} />
                </Col>
            </Row>
        </>
    );
}

实时示例:

const { useState, useEffect, useMemo, useContext } = React;

const Row = props => <div {...props}/>;
const Col = props => <div {...props}/>;
const ProjectOverview = ({projectContext, project}) => {
    return <div>Project: {project.name}</div>;
};

const contextValue = {
    getProject(handle) {
        console.log(`Getting project ${handle}...`);
        // busy-wait half a second
        const end = Date.now() + 500;
        while (Date.now < end) {
            // Wait -- NEVER BUSY WAIT LIKE THIS IN REAL CODE
        }
        return {name: `Project for handle ${handle}`};
    }
};
const ProjectContext = React.createContext(contextValue);

const Project = ({ match }) => {
    console.log(`Project called with handle = ${match.params.handle}`);

    const projectContext = useContext(ProjectContext)
    const project = useMemo(
        () => {
            console.log(`Recalcuating project from handle ${match.params.handle}`);
            return projectContext.getProject(match.params.handle);
        },
        [match.params.handle]
    );

    console.log(`project is: "${project.name}"`);
   
    // Note: Can't use <>...</> in Stack Snippets, we have
    // to use the older React.Fragment syntax.
    return (
        <React.Fragment>
            <Row>
                <Col sm={12} lg={12}>
                    <ProjectOverview projectContext={projectContext} project={project} />
                </Col>
            </Row>
        </React.Fragment>
    );
}

const App = () => {
    const [counter, setCounter] = useState(0);
    const [handle, setHandle] = useState(1);
    
    useEffect(() => {
        if (handle < 3) {
            setTimeout(() => {
                setCounter(c => {
                    c = c == 0 ? 1 : 0;
                    if (c == 0) {
                        // Update the handle every two calls
                        setHandle(h => h + 1);
                    }
                    return c;
                });
            }, 800);
        }
    }, [handle, counter]);

    return (
        <ProjectContext.Provider value={contextValue}>
            <Project match={{params: {handle}}} />
        </ProjectContext.Provider>
    );
};

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

请注意,getProject仅在句柄更改时才被调用,而不是在每次Project组件函数被调用时才被调用。


如果,您需要使用道具来初始化状态,here's what the React docs have to say about it

如何实施getDerivedStateFromProps

虽然您可能不需要它,但在极少数情况下(例如实现<Transition>组件),可以在渲染过程中立即更新状态。退出第一个渲染后,React会立即以更新后的状态重新运行组件,这样就不会太昂贵了。

在这里,我们将行prop的先前值存储在状态变量中,以便我们可以进行比较:

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

您的操作方式无效的原因是,您传递的useState值仅在第一次时用于组件函数,而不是每次都使用时间被称为。在所有后续调用中,将使用为组件存储的状态值。这就是useState的目的:维持在组件的生命周期内发生变化的状态。

下面是一个示例,显示了在创建组件后props如何更改,以及如何通过useState复制prop到状态是不正确的(除非您想记住prop的第一个值) :

const { useState, useEffect } = React;

const Example = ({propValue}) => {
    const [stateValue] = useState(propValue); // <== Usually not correct!
    
    console.log(`propValue = ${propValue}, stateValue = ${stateValue}`);
    
    return (
        <div>
            <div><code>propValue = {propValue}</code></div>
            <div><code>stateValue = {stateValue}</code></div>
        </div>
    );
};

const App = () => {
    const [counter, setCounter] = useState(0);
    
    useEffect(() => {
        if (counter < 10) {
            setTimeout(() => {
                setCounter(c => c + 1);
            }, 800);
        }
    }, [counter]);
    
    return <Example propValue={counter} />;
};

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

注意stateValue从未增加的原因,因为一旦使用了初始值,就使用了组件在React管理状态信息中的值。

首次创建组件实例时,它基本上会获得一个插槽数组,每个调用的钩子都按它们被调用的顺序分配一个插槽(这就是为什么钩子调用必须始终以相同的顺序进行)。该数组中的数据会在您的组件函数的首次调用期间填充,然后在每次调用该函数以更新组件实例时重新使用。您可以很随意地想到useState,如下所示的伪代码:

function useState(initialValue) {
    if (componentState[hookIndex]) {
        return componentState[hookIndex];
    }
    return componentState[hookIndex] = [initialValue, createSetter(hookIndex)];
}

......,其中componentState是组件实例信息的数组,而hookIndex是此挂钩调用的索引,按照组件函数中的调用顺序。

Dan Abramov的

This article对于理解这些内容确实很有帮助。表面上是关于useEffect的,但实际上它从钩子的角度解释了钩子如何工作以及组件的生命周期。