1. Background
In the ancient days, when there was no separation of front and back end development, filter action filters on a page typically changed the URL synchronously, triggering a browser refresh that correctly rendered the “selected filter value” filter and the list data filtered by the filter.
The flow of this rendering mode is as follows:
This rendering process interconnects filters and routing, enabling the user to reproduce the “previous page” when “back to the page”.
This was fine in the olden days, and users who were new to “surfing the web” didn’t have much need to experience it.
However, with the development of the Internet, the amount of information has skyrocketed, and the amount of data presented by a page with screening ability has become increasingly rich, and the user’s experience demand is also more “tricky”.
Under this change, the “browser refresh” triggered by the “filter action” obviously cannot satisfy the status quo, because the “refresh” action has the following disadvantages:
- Multiple repeated requests
- The response time is too long
- Page flash problem
Now that the front and back ends are separated, pages are more complex and there is a greater need to avoid page refreshes. In order to realize the ability of filtering and routing linkage, a new scheme needs to be found.
2. Solutions
The HTML5 standard makes History even more powerful, including the ability of a History API called replaceState to change a URL without triggering a browser refresh.
An introduction to the replaceState can be found at developer.mozilla.org/zh-CN/docs/…
The rendering process of filtering and routing linkage after replaceState transformation is as follows:
3. Implementation details
To realize filtering and routing linkage, React based rendering mechanism (filter value changes will setState, thus triggering the update process), our code logic is roughly as follows:
import React, { useMemo, useCallback } from "react";
const useList = buildUseList({
getData: fetchData(),
});
const Cmpt: React.FC = (props) = > {
// Parse the URL to get the initialization filter value
const initQuery = useMemo(() = > {
const search = new URLSearchParams(location.search);
return JSON.parse(search.get("query")); } []);// Initialize the request
const [listResult, listAction] = useList(props, initQuery);
const handleSyncUrlSearchParam = useCallback(
(newQuery) = > {
// Synchronize the new filter value to the route query parameter
history.replaceState(null."".`? query=The ${JSON.stringify(newQuery)}`);
// Updating filter values triggers a re-request
listAction.setQuery(newQuery);
},
[listAction]
);
return (
<>{/* Filter */}<Filter defaultValue={initQuery} onChange={handleSyncUrlSearchParam} />{/* Data render */}<List dataSource={listResult.list} />
</>
);
};
export default Cmpt;
Copy the code
Analyzing the logic above, we can implement a generic Hook named “useQuery” that provides the following capabilities:
- Parse the URL to get the initialization filter value
- Listen for changes in dependency values to be synchronized to route query parameters
The Hook is implemented as follows:
import { useEffect, useMemo, useRef, useState } from 'react';
import { shallowEqual } from '@byted-pick/common-utils';
type IOptions = {
/** Query the name */name? :string;
/** Convert method */transform? : {/** Encoding method */
encode: (query: any) = > string;
/** Decoding method */
decode: (str: string) = > any;
};
};
/** ** the factory function@param Options Query conditions Hook configuration *@returns* /
export function buildUseQuery(options? : IOptions) {
const {
name = 'query',
transform = {
encode: (query: any) = > encodeURIComponent(JSON.stringify(query)),
decode: (str: string) = > JSON.parse(decodeURIComponent(str)),
},
} = options ?? {};
/** * Hook *@param DefaultQuery defaultQuery condition *@param GetDept listens for dependencies *@returns* /
function useQuery(defaultQuery? :any, getDept? : () = >any) {
const _defaultQuery = useMemo(() = > {
const search = new URLSearchParams(location.search);
let existQuery = {};
const value = search.get(name);
if (value) {
try {
existQuery = transform.decode(value);
} catch {
// DO NOTHING}}return {
...defaultQuery,
...existQuery,
};
}, []);
const [query, setQuery] = useState(_defaultQuery);
const cacheQuery = useRef<any> (); useEffect(() = > {
if(! getDept) {return;
}
const deptQuery = getDept() ?? {};
// Shallow comparison object
if (shallowEqual(deptQuery, cacheQuery.current)) {
return;
}
cacheQuery.current = deptQuery;
// Filter invalid query parameters
const newQuery = Object.keys(deptQuery).reduce((p, k) = > {
const v = deptQuery[k];
switch (true) {
case [' '.null.undefined].includes(v):
// Filter empty data
break;
case typeof v === 'object' && Object.keys(v).length <= 0:
// Filter empty objects
break;
default:
p[k] = v;
break;
}
return p;
}, {} as Record<string.any>);
const search = new URLSearchParams(location.search);
if (Object.keys(newQuery).length <= 0) {
search.delete(name);
} else {
search.set(name, transform.encode(newQuery));
}
history.replaceState(null.' '.`?${search.toString()}`);
});
return [query, setQuery];
}
return useQuery;
}
Copy the code
The code after the Hook transformation is as follows:
import React, { useMemo, useCallback } from "react";
const useList = buildUseList({
getData: fetchData(),
});
const useQuery = buildUseQuery();
const Cmpt: React.FC = (props) = > {
// Parse the URL to get the initial filter value and listen for changes in the dependent value to be synchronized to the route query parameter
const [initQuery] = useQuery({}, () = > listResult.query);
const [listResult, listAction] = useList(props, initQuery);
return (
<>{/* Filter */}<Filter defaultValue={initQuery} onChange={listAction.setQuery} />{/* Data render */}<List dataSource={listResult.list} />
</>
);
};
export default Cmpt;
Copy the code
4. Expand content
You should note that “useQuery” is built in “build” mode. It supports passing in an “options” configuration.
4.1 Customizing the Query name
By default, useQuery uses Query as the name of the route query condition. The result is as follows:
The new project is not a problem because we reserve this “name” for the filtering logic.
However, when “useQuery” is used in the transformation of “old project”, the “name” of “Query” may be occupied by the existing component logic, while the reconstruction of old project is recommended only when there is a large ROI, and the development is generally carried out in an iterative way.
“UseQuery” also happens to support custom “name” values, as shown in the following code:
const useQuery = buildUseQuery({
// Customize the query name, default is "query"
name: 'my_query'});Copy the code
The effect is as follows:
4.2 User-defined Route Query Criteria Converter
The filter value is an object type value, and the route query condition is a string that needs to meet the URI specification. There must be conversion logic between these values.
UseQuery converts it using JSON serialization plus URI serialization by default. This transformation logic meets the requirements of general development.
However, readers who are keen on perfection should be aware that the results of this conversion are “too long”, as in the “custom query name” image above, where the user simply filters the time range:
As you can see from the results of “URI deserialization”, the “URI serialization” process serializes “parentheses”, “quotes”, and “colons”, which encode long urIs, there is clearly room for optimization.
The “useQuery” also supports custom configuration of the “converter” to achieve optimization, example code is as follows:
const useQuery = buildUseQuery({
// Customize the query name, default is "query"
transform: {
encode: (query: any) = > {
// TODO converts filter values to routing query values
return str;
},
decode: (str: string) = > {
// TODO replaces the route query value with the filter value
returnquery; ,}}});Copy the code
This conversion optimization logic is not implemented in this article, left to the readers to implement ha ~
5. The latter
Readers of other articles in the React General Solution series should note that the author’s code often uses the “buildXXX” function name.
This “build” is a very interesting programming idea, which I will keep in mind in the next chapter, “React Common Solutions — Component Generators”.