首页 > 解决方案 > NextJS - 使用浅路由器推送时无法识别页面组件上发生状态变化的位置

问题描述

我有一个应用程序。使用 NextJS。我有一个如下所示的页面:

import React from 'react'
import { parseQuery } from '../lib/searchQuery'
import Search from '../components/search'

class SearchPage extends React.Component {
  static getInitialProps ({ query, ...rest }) {
    console.log('GET INITIAL PROPS')
    const parsedQuery = parseQuery(query)
    return { parsedQuery }
  }

  constructor (props) {
    console.log('CONSTRUCTOR OF PAGE CALLED')
    super(props)
    this.state = props.parsedQuery
  }

  render () {
    return (
      <div>
        <div>
          <h1>Search Results</h1>
        </div>
        <div>
          <h1>DEBUG</h1>
          <h2>PROPS</h2>
          {JSON.stringify(this.props)}
          <h2>STATE</h2>
          {JSON.stringify(this.state)}
        </div>
        <div>
          <Search query={this.state} />
        </div>
      </div>
    )
  }
}

export default SearchPage

getInitialProps为 SSR 运行 - 它接收查询字符串作为对象(通过后端的 Express)通过一个简单的“清洁”功能运行它parseQuery- 我制作了它,并通过 props 将其注入页面,props.parsedQuery如您在上面看到的. 这一切都按预期工作。

Search组件是一个包含许多字段的表单,其中大部分select基于预定义字段和一些基于数字的input字段,为简洁起见,我省略了整个组件的标记。Search获取道具并通过函数query将它们分配给其内部状态。constructor

在更改组件上的selectinput字段时,将Search运行此代码:

this.setState(
  {
    [label]: labelValue
  },
  () => {
    if (!this.props.homePage) {
      const redirectObj = {
        pathname: `/search`,
        query: queryStringWithoutEmpty({
          ...this.state,
          page: 1
        })
      }
      // Router.push(href, as, { shallow: true }) // from docs.
      this.props.router.push(redirectObj, redirectObj, { shallow: true })
    }
  }
)

这里的意图是 CSR 接管 - 因此是肤浅的router.push。页面 URL 更改但getInitialProps不应再次触发,后续查询更改通过componentWillUpdateetc. 处理。我确认getInitialProps由于缺少相应触发而不会再次console.log触发。

问题

但是,在检查/取消选中Search组件上的选择字段时,我惊讶地发现状态SearchPage仍在更新,尽管没有this.setState()被调用的证据。

constructor没有被调用,也没有getInitialProps,所以我不知道是什么导致状态改变。

在初始 SSR 之后,调试块如下所示:

// PROPS
{
  "parsedQuery": {
    "manufacturer": [],
    "lowPrice": "",
    "highPrice": ""
  }
}
// STATE
{
  "manufacturer": [],
  "lowPrice": "",
  "highPrice": ""
}

然后在检查一个选择字段后Search令人惊讶地更新为:

// PROPS
{
  "parsedQuery": {
    "manufacturer": ["Apple"],
    "lowPrice": "",
    "highPrice": ""
  }
}
// STATE
{
  "manufacturer": ["Apple"],
  "lowPrice": "",
  "highPrice": ""
}

我找不到对这种行为的解释,没有任何东西输出到控制台,我也找不到如何通过 dev 跟踪状态更改的来源。工具。

当然,只有当我通过componentDidUpdate? 而且真的不应该parsedQuery只更新道具getInitialProps吗?因为这就是创建和注入它的原因?

为了进一步增加混淆,如果我更改(例如)input上的数字字段,URL 会按预期更新,但调试块中的道具和页面状态会发生变化。无法理解这种不一致的行为。SearchlowPrice

这里发生了什么?

编辑

我已经添加了一个回购。它在 GitHub 上将这个问题作为 MWE 重现,您可以在此处克隆它:problem MWE repo。

标签: javascriptreactjsnext.js

解决方案


哇,有趣的问题。这是一个有趣的小谜题。

TL;DR:这是你的错,但你是如何做到的却很微妙。首先,问题出在这一行: https ://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L17

在这个例子中,它是这样的:

this.state = props.parsedQuery

让我们考虑一下那里实际发生的事情。

在 IndexPage.getInitialProps 中,您正在执行以下操作:`

const initialQuery = parseQuery({ ...query })
return { initialQuery }

通过 Next 的机制,这些数据通过 App.getInitialProps 以作为 pageProps.initialQuery 返回,然后在 IndexPage 中成为 props.initialQuery,然后被批发到您的搜索组件 - 然后您的搜索组件“制作副本" 来避免改变它。都很好,对吧?

你错过了什么。

在 lib/searchQuery.js 中有这一行:

searchQuery[field] = []

同一个数组被传递到 Search 中——除非你没有复制它。您正在复制 props.query - 其中包含对该数组的引用。

然后,在您的搜索组件中,当您更改复选框时执行此操作:

const labelValue = this.state[label]

https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L57

你正在改变你在构造函数中“复制”的数组。你正在直接改变你的状态!这就是 initialQuery 似乎在主页上更新的原因——您改变了 initialQuery 引用的制造商数组——它从未被复制。您拥有在 getInitialProps 中创建的原始参考!

您应该知道的一件事是,即使在浅推送时未调用 getInitialProps,App 组件仍会重新渲染。它必须为了反映路由更改到消费组件。当您在内存中更改该数组时,您的重新渲染会反映更改。添加价格时,您不会改变 initialQuery 对象。

这一切的解决方案很简单。在 Search 组件构造函数中,您需要查询的深层副本:

this.state = { ...cloneDeep(props.query) }

做出改变,问题就消失了,您在打印输出中不再看到 initialQuery 发生变化 - 正如您所期望的那样。

您还想更改它,即直接访问您所在州的数组:

const labelValue = this.state[label]

对此:

const labelValue = [...this.state[label]]

为了在更改数组之前复制数组。你通过立即调用 setState 来掩盖这个问题,但实际上你是在直接改变你的组件状态,这将导致各种奇怪的错误(比如这个)。

之所以出现这种情况,是因为您的组件状态中有一个全局数组正在发生突变,因此所有这些突变都反映在不同的地方。


推荐阅读