我有一条路线:
<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? (我是有点新来反应(和反应钩子))
答案 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
是此挂钩调用的索引,按照组件函数中的调用顺序。
This article对于理解这些内容确实很有帮助。表面上是关于useEffect
的,但实际上它从钩子的角度解释了钩子如何工作以及组件的生命周期。