React路由器,如何在浏览器后退按钮后恢复状态?

37
我正在研究如何使我的React SPA应用程序在用户使用浏览器的前进/后退按钮导航时保持状态。例如,用户正在填写表单,然后他前后移动,表单会自动恢复。
我查看了许多JavaScript路由器,但似乎没有一个能正确解决这个问题...(甚至根本不考虑)。
目前,我正在使用React Router 4,并采用以下模式:
1. 当以编程方式离开页面时,我首先使用history.replace(this.state)保存当前页面的状态;然后我使用history.push(newLocation)移出页面。 2. 当一个页面组件被创建(componentWillMount)时,我检查this.props.location.state !== undefined,如果是,则使用this.setState(this.props.location.state)恢复它。
我的问题是:上述模式是否正确?是否建议这样做?有没有更好且广泛采用的方法?

根据文档history.pushhistory.replace都具有相同的函数签名(path,[state])。那么为什么您要使用带有状态作为第二个参数的history.replace呢?我认为您应该使用简单的history.push(newLocation, this.state)而不是这两个调用。 - Dane
history.push 中的状态参数是指“在按下后退按钮时将恢复的当前页面状态”还是“新页面将采取的状态”?这非常令人困惑。 - nino.porcino
更像是第二个,我猜.. 它是一些你想要传递到下一页的状态。所以在你的情况下,我认为你可以传递表单的状态。 - Dane
1
好的,@Dane,我明白了,我会将状态传递给新页面。但我不明白的是如何处理返回按钮。当用户点击它时,我没有任何机会运行任何代码。我应该在每次this.setState()之后使用history.replaceState()来保持当前状态更新(同步)吗? - nino.porcino
2
我认为你需要使用一些状态管理库,比如Redux来实现这个功能,因为在React Router中维护跨路由的状态并不是被完全支持的。你可以将状态传递给下一个页面,但仅限于此。 - Dane
5个回答

19

一段时间后,我找到了一个合理的解决方法:

  • 在React中,每次this.setState()之后,我使用window.history.replaceState({ key: history.location.key, state: this.state})将状态与历史记录同步。
  • 当“页面”组件被挂载或刷新(willMountwillReceiveProps)时,我检查props.params.location.state中的状态:如果存在,则恢复它;如果不存在,则创建一个全新的状态。
  • 当在同一页上导航时,我不使用路由,只使用this.setStatewindow.history.pushState
  • 当导航到页面之外时,我只使用路由,并避免在状态中传递任何内容。

这个解决方案看起来很好用,唯一的小缺点是:

  • 状态必须是可序列化的。
  • this.setState是一个陷阱,因为它是异步的,除非你做技巧性处理,否则不能在其后面使用this.state
  • 在恢复或初始化阶段,必须通过一个函数提供初始空状态,它不能只停留在Componentconstructor()中。
  • 在Firefox中,后退按钮后自动滚动恢复的效果随机出现,不知道为什么,但Chrome和Edge都没有问题。

总的来说,我编写了一个继承ComponentPageComponent,它可以执行所有的初始化/恢复工作;它还覆盖了this.setState,使其自动与历史记录同步,并避免了异步方面的麻烦。


这个建议非常有帮助。为了解决setState同步问题,您可以传递一个回调函数来替换状态。this.setState({ a: "b" }, function() { console.log("replace state here") }); - Philip Giuliani
我喜欢这种方法。但是据我所知,history.state 有大小限制。我的做法是: 在每次 setState 完成后,将结果缓存到 sessionStorage 中,2. 每次组件加载时,检查 history.action==="POP",如果为 true,则加载缓存的状态版本而不是从服务器获取。目前看来是可以正常工作的,但我发现当前的缺点是,如果用户使用 F5 或浏览器按钮重新加载页面,则 history.action 仍然是 "POP"。 - elMeroMero

1

你也可以使用会话存储而不是状态。只需替换 useState() 函数并使用 useSessionStorage()

例如:


const [myState, setMyState] = useSessionStorage("state_key", "initial_value")


const handleSomeChangeOnState = (event: any) => {
  setMyState("new_value")
}

0

当React组件卸载时,状态也会被销毁。https://beta.reactjs.org/learn/preserving-and-resetting-state

您可以使用Redux https://redux.js.org/,这是一个状态管理库,从组件中提取状态。这样,即使您的组件卸载,只要Redux Provider保持挂载状态(如果您在应用程序的顶层设置了提供程序,则应该是这种情况),状态将保持不变。

Redux切片示例

const mySlice = createSlice({
  initialState: { textFieldA: '' },
  name: "textFields",
  reducers: {
    setTextFieldA(state, action) {
      state.textFieldA = action.payload;
    }
  }
});


你需要创建一个 Redux store 并将你的 reducer 添加到它里面。
在你的 React 组件中,使用 Redux slice。
import { setTextFieldA } from './slice'

function MyComponent() {
  const { textFieldA } = useSelector((state) => state.myReducer);

  function onTextFieldAChange(text) {
    dispatch(setTextFieldA(text));
  }

  // render component
}

现在,如果MyComponent卸载,状态将在我导航到应用程序的不同部分时保留在存储中。当我重新渲染MyComponent时,textFieldA将保持与离开时相同的状态。


0
使用getDerivedStateFromProps并检查props.location.key的值是否发生变化,以便仅在用户使用浏览器按钮导航时恢复状态。
 static getDerivedStateFromProps(props,states){
         if ((typeof props.location !== 'undefined') &&
             (typeof props.location.state !== 'undefined') &&
              (states.location_key !== props.location.key)) {
             let newState = props.location.state;
             newState.location_key = props.location.key;
             return newState;
         }
    }

-1

我也思考过这个问题, 例如,您有一个多步骤表单,并且您希望通过浏览器控件在步骤之间导航。结果,您必须使用路由器来存储activeStep,但是您不想在浏览器URL中共享它。解决方案是将步骤存储在路由器状态中:

const location = useLocation();
const history = useHistory();

const onSubmit = useCallback(() => {
  history.push({
    ..location,
    state: {
      activeStep: 1
    }
  });
}, [...]);

因此,您的步骤将在页面刷新后恢复事件,但您无法与其他用户共享步骤。这很完美。

下一个问题是在步骤之间存储表单数据。我们也不能将其存储在路由器状态中。我选择了sessionStorage来存储它。它具有类似于路由器状态的属性。因此,您必须在提交时更新会话存储,并在初始化表单时检索数据:

const location = useLocation();
const history = useHistory();
const [initData] = useState(getFormDataFromStorage());

const onSubmit = useCallback(() => {
  saveFormDataToStorage(...);
  history.push({
    ..location,
    state: {
      activeStep: 1
    }
  });
}, [...]);

我已经准备好了演示GitHub,您可以在这里测试它,并且您可以在这里RU)找到详细信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接