UNPKG

sync-query

Version:

Use the React higher-order component to store the React State in URL query (routing parameters)

181 lines (172 loc) 7.81 kB
import { pick, debounce } from "../helpers/util"; import { queryToState, stateToQuery, IQueryParser, IQueryStringify } from "../helpers/convert"; import { filterQuery } from "../helpers/url"; import { isArray, isObject } from "../helpers/type"; import { deepEqual } from "../helpers/deepEqual"; export function syncQueryCb(callbackDeps?:string[]) { return function (target: any, propertyKey: string) { target.callbackName = propertyKey; if (isArray(callbackDeps) && callbackDeps.length > 0) { target.callbackDeps = callbackDeps; } }; } const __SYNC_QUERY_DIFF_IGNORE__ = '__SYNC_QUERY_DIFF_IGNORE__'; const __SYNC_QUERY_CALLBACK_IGNORE__ = '__SYNC_QUERY_CALLBACK_IGNORE__'; export function SyncQueryFactory(stateList: string[], callbackName?:string, config?:SyncQueryConfig) { return function(WrappedComponent) { return syncQueryHOC(WrappedComponent, stateList, callbackName, config); } } type SyncQueryConfig = { wait?: number, // 函数防抖的等待时间, 单位 ms callbackDeps?: string[], // callbackDeps 存放 state key 的数组,监听到 state 中对应key 的 value 变化时,会调用 callback(网络请求等) // PS: callbackDeps 没有传入时,默认监听的内容等于 stateList parser?: IQueryParser, // 解析器:用于将路由参数 query 解析到 state,默认是 JSON.parse stringify?: IQueryStringify, // 生成器:用于生成 state 对应的 query 字符串,默认是 JSON.stringify disableAutoSync?: boolean, // 是否关闭自动同步 } export interface SyncQueryHost { triggerSync } /** * syncQueryHOC * @param WrappedComponent * @param stateList states are observed * @param callbackName callbackName would be called when state difference is detected * @param config SyncQueryConfig */ export function syncQueryHOC(WrappedComponent, stateList: string[], callbackName?:string, config?:SyncQueryConfig) : any{ if (!isObject(config)) { config = { wait: 300, } } else { config = { wait: 300, ...config, } } return class Enhancer extends WrappedComponent { private prevStateCache = {}; constructor(param) { super(param); this.state = { ...this.state, ...this.getStateFromURL(stateList), } this.reBindCallback(false, true); this.prevStateCache = this.state; if (config.disableAutoSync === true) { this.triggerSync = () => { this.stateDiffEffect(this.state); } } this.stateDiffEffect = debounce(this.stateDiffEffect, config.wait).bind(this); } private getStateFromURL(stateList:string[]) { const query = location.href.split('?')[1]; if (query == null) { return; } return queryToState(query, stateList, config.parser); } private syncStateToURL(state:Object) { const [locationAddress, oldQuery] = location.href.split('?'); const restQuery = filterQuery(oldQuery, (key, value) => (stateList.indexOf(key) === -1)) const query = stateToQuery(state, config.stringify); const href = `${locationAddress}?${query}&${restQuery}`; location.href = href; } private reBindCallback(diffIgnore:boolean = true, callBackIgnore?:boolean) { this.callbackName = this.callbackName || callbackName; if (this.callbackName == null) { return; } if (typeof super[this.callbackName] !== 'function') { console.error('sync-query: callback must be react component method name!!! Tips: SyncQueryFactory and syncQueryHOC must be closest with Component Class'); return; } const clone = Object.create(this); clone.setState = (updater, callback) => { this.setState(updater, callback, diffIgnore, callBackIgnore); } // super[callbackName] is super.prototype[callbackName], so it is not bound with _super. this[this.callbackName] = super[this.callbackName].bind(clone); this.callbackDeps = this.callbackDeps || config.callbackDeps; } componentDidMount() { const result = super.componentDidMount && super.componentDidMount(); this.reBindCallback(); return result; } componentDidUpdate(prevProps, prevState) { if (this.state[__SYNC_QUERY_DIFF_IGNORE__] === true || config.disableAutoSync === true) { return ( super.componentDidUpdate && super.componentDidUpdate(prevProps, prevState) ); } this.stateDiffEffect(this.state); return ( super.componentDidUpdate && super.componentDidUpdate(prevProps, prevState) ); } private stateDiffEffect(state) { const prevState = this.prevStateCache; if (prevState == null && state == null) { console.error('sync-query: stateDiffEffect could not be null'); return; } // stateList diff const pickedPrevState = pick(prevState, stateList); const pickedState = pick(state, stateList); const isDiff = !deepEqual(pickedPrevState, pickedState); if (isDiff) { this.syncStateToURL(pickedState); } if (this.state[__SYNC_QUERY_CALLBACK_IGNORE__] === true) { console.warn('Ingore: sync-query: auto trigger callback,only after ComponentDidMount'); return; } // callbackDeps diff const callbackDeps = this.callbackDeps; if (callbackDeps == null) { isDiff && this[this.callbackName] && typeof this[this.callbackName] === 'function' && this[this.callbackName](); } else { const pickedPrevState = pick(prevState, callbackDeps); const pickedState = pick(state, callbackDeps); const isDiff = !deepEqual(pickedPrevState, pickedState); isDiff && this[this.callbackName] && typeof this[this.callbackName] === 'function' && this[this.callbackName](); } this.prevStateCache = state; } setState(updater, callback?:() => void, diffIgnore?:boolean, callBackIgnore?:boolean) { // Ref: https://zh-hans.reactjs.org/docs/react-component.html#setstate if (typeof updater === 'object') { return super.setState( { ...updater, [__SYNC_QUERY_DIFF_IGNORE__]: diffIgnore, [__SYNC_QUERY_CALLBACK_IGNORE__]: callBackIgnore, }, callback, ); } if (typeof updater === 'function') { return super.setState( function (state, props) { return { ...updater(state, props), [__SYNC_QUERY_DIFF_IGNORE__]: diffIgnore, [__SYNC_QUERY_CALLBACK_IGNORE__]: callBackIgnore, } }, callback, ); } return super.setState(updater, callback); } } }