React Redux SSR如何抑制从componentDidMount调用的初始操作?

时间:2017-01-29 09:42:13

标签: reactjs redux react-redux serverside-javascript

我能够在服务器端预加载所有需要的状态,并将这些初始状态传递给客户端应用程序的redux存储。

我已经启用了redux-logger来查看我的应用程序中发生了什么,并且正在从商店中重新获取状态。那是。因为我的组件在componentDidMount期间调用必要的操作。这是一个示例reducer,action和component:

// action:
import axios from 'axios';

export function fetchNewsTop() {
    return {
        type: 'FETCH_NEWS_TOP',
        payload: axios.get('/news/top')
    };
}

// reducer:
export function newsArchive(state = {
    pending: false,
    response: { data: [] },
    error: null
}, action) {
    switch (action.type) {
    case 'FETCH_NEWS_TOP_PENDING':
        return { ...state, pending: true, response: { data: [] }, error: null };
    case 'FETCH_NEWS_TOP_FULFILLED':
        return { ...state, pending: false, response: action.payload.data, error: null };
    case 'FETCH_NEWS_TOP_REJECTED':
        return { ...state, pending: false, response: { data: [] }, error: action.payload };
    default:
        return state;
    }
}

// component:
export class NewsArchiveFactory extends React.Component {

    componentDidMount() {
        this.props.fetchNewsTop();
    }

    render() {
        if (this.props.news) {
            return (
                <NewsGrid items={this.props.news} />
            );
        }
        return null;
    }
}

我正在使用redux-promise-middleware,它会创建承诺操作(已完成,已拒绝,待处理)。

安装组件时会调用该操作。我的理解是,即使在服务器端呈现组件以通知JS它存在,也会调用componentDidMount。组件本身不会被重新安装。但是,我还读到,在componentDidMount中运行我的操作是一个更好的选择,我个人认为如果我从componentDidUpdate调用它,它将无法工作(我们将进入无限循环)

我想抑制这些初始操作被调用,因为状态已经来自服务器端。我怎样才能做到这一点?

1 个答案:

答案 0 :(得分:0)

我希望有更好的方法,但到目前为止,我所提出的是在收到数据时根据是否收到数据在reducer状态中设置ssr标志在服务器或客户端上。

首先,ComponentDidMount不会在服务器上触发,仅在客户端上触发。因此,您将需要另一种机制,它使用类似react-router匹配函数的内容以及组件中的静态数据获取函数。有关具有易于理解的服务器端数据获取机制的良好样板,请参阅universal-react。但是,您仍然需要在ComponentDidMount方法中获取数据,或者如果Component仅在客户端上呈现或在初始服务器呈现之后安装,则会中断。这就是引入潜在问题的原因&#34; double fetches&#34;当数据在服务器上预先加载但在ComponentDidMount在客户端上触发时再次请求。

下面是一个解决此问题的示例实现(请注意,我使用的是redux格式,其中actions和reducers在一个文件中):

终极版/模块/ app.js:

import { basepath, fetcher, fetchNeeded, checkStatus } from '../../lib/api'

const isClient = typeof document !== 'undefined'

const SET_FLAG_SSR =            'app/SET_FLAG_SSR'
const REQUEST_DATA =            'app/REQUEST_DATA'
const RECEIVE_DATA =            'app/RECEIVE_DATA'
const DATA_FAIL =               'app/DATA_FAIL'
const INVALIDATE_DATA =         'app/INVALIDATE_DATA'

const initialState = {
  ssr: false,
  initialized: false,
  isFetching: false,
  didInvalidate: true,
  conditionFail: false,
  data: {}
}

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case SET_FLAG_SSR:
      return Object.assign({}, state, {
        ssr: action.flag
      })
    case INVALIDATE_DATA:
      return Object.assign({}, state, {
        didInvalidate: true
      })
    case REQUEST_DATA:
      return Object.assign({}, state, {
        isFetching: true,
        conditionFail: false,
      })
    case RECEIVE_DATA:
      return Object.assign({}, state, {
        ssr: !isClient,
        initialized: true,
        isFetching: false,
        conditionFail: false,
        didInvalidate: false,
        lastUpdated: action.receivedAt,
        data: action.data,
      })
    case DATA_FAIL:
      return Object.assign({}, state, {
        isFetching: false,
        conditionFail: true,
      })
    default:
      return state
  }
}

function setFlagSSR(flag) {
  return {
    type: SET_FLAG_SSR,
    flag: flag,
  }
}

function requestData() {
  return {
    type: REQUEST_DATA
  }
}

function receiveData(json) {
  return {
    type: RECEIVE_DATA,
    data: json,
    receivedAt: Date.now()
  }
}

function receiveFail(err) {
  return {
    type: DATA_FAIL,
    err: err,
  }
}

function fetchData(url) {
  return (dispatch, getState) => {
    dispatch(requestData())
    return fetcher(url, { credentials: 'include' }, getState().cookie)
    .then(response => response.json())
    .then((json) => { return dispatch(receiveData(json)) })
    .catch(err => {
      dispatch(receiveFail(err))
    })
  }
}

export function getData() {
  return (dispatch, getState) => {
    const state = getState()
    if (fetchNeeded(state.app, () => dispatch(setFlagSSR(false)))) {
      return dispatch(fetchData(basepath + '/api/app'))
    }
  }
}

LIB / api.js:

import fetchPonyfill from 'fetch-ponyfill'
import Promise from 'bluebird'

const { fetch, Request, Response, Headers } = fetchPonyfill(Promise)

const isClient = typeof document !== 'undefined'
export const basepath = isClient ? '': `${__APIROOT__}`

// check HTTP response status and throw error if not valid
export function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response
  } else {
    let error = new Error(response.statusText)
    error.response = response
    throw error
  }
}

// Generic Redux check to triage fetch requests and SSR flags
export function fetchNeeded(statebranch, clearFlagSSR) {
  if (statebranch.isFetching) return false
  if (statebranch.ssr && isClient) {
    clearFlagSSR()
    return false
  }
  return true
}

// Fetch wrapper to set defaults and inject cookie data on server (if present)
export function fetcher(url, config={}, cookie={}) {
  let options = {}
  let headers = new Headers();

  // Base Headers
  headers.set('Accept', 'application/json');
  headers.set('Content-Type', 'application/json');

  if (config.headers) {
    Object.getOwnPropertyNames(config.headers).forEach(function(key, idx, array) {
      headers.append(key, config.headers[key])
    })
  }

  if (cookie.value && !isClient) {
    //inject cookie data into options header for server-to-server requests
    headers.set('Cookie', cookie.value)
  }

  options = {
    credentials: config.credentials || 'same-origin',
    method: config.method || 'get',
    headers: headers,
  }

  if (config.body) options.body = config.body

  return fetch(url, options)
}

这是如何工作的基础是如果服务器上发生ssr操作,则redux状态中的true标志设置为RECEIVE_DATA。否则设置为false

如果在客户端上调用getData函数,它首先调用fetchNeeded函数(将状态树的这个特定片段与可以调度操作的函数一起传递给ssr函数以清除{ {1}}旗帜)。如果ssr标记为true(当然我们在客户端),fetchNeeded函数将在调用函数清除false后返回ssr标志。

所以最后,如果服务器调用我们的getData函数和数据已经存在,那么客户端第一次就不会从服务器重新请求数据(而是清除{{ 1}}旗帜)。标志清除后(或者如果它从未在第一个位置设置),客户端将像正常一样从服务器请求数据。这允许我们避免对数据进行初始冗余调用,但如果组件仅安装在客户端上或在客户端上刷新,则仍然有效。

希望这很有帮助。