preface
In the selection of technology for new projects, some thinking records are made for data flow.
A large number of articles have analyzed the advantages and disadvantages of data flow solutions such as REdux and MOBx. In my opinion, the choice of solutions cannot be separated from business.
After react hooks, there are many great solutions like SWR.
As a developer, there is always a question: should the current data be put into the state management repository or used only in components? And we always uphold the idea:
- When data does not need to be shared, it should belong to a component, keep it independent, do not need to use state to manage it, and then discard it.
- Avoid boilerplate code, too much redundant code increases application complexity
After fully embracing react hooks, Redux contradicts that idea. We need a simpler, leaner, more view-tight requester (this article assumes you already have some knowledge of SWR)
First, let’s look at how the React Function Component obtains data:
function Posts() {
const [posts, setPosts] = useState();
const [params, setParams] = useState({id: 1});
useEffect(() = > {
fetchPosts(params).then(data= > setPosts(data));
}, [params]);
if(! posts) {return <div>Loading posts...</div>; }
return (
<ul>{posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul>
);
}
Copy the code
As you can see, in the case of hooks, requesting data requires additional logic to hold the state of the data stream, which adds more redundancy and some logic extensions are missing.
With this in mind, we can wrap the above request logic into custom hook simplification logic (pseudocode) :
const useRequest = (api, params) = > {
const [posts, setPosts] = React.useState()
const [loading, setLoading] = React.useState(false)
const fetcher = (params) = > {
setLoading(true)
const realParmas = JSON.parse(params)
api(realParmas).then(data= > {
setPosts(data)
setLoading(false)
})
}
React.useEffect(() = > {
fetcher(params)
}, [params])
return {
data: posts,
loading
}
}
function Posts() {
const { data, loading } = useRequest('/user'.JSON.stringify({params: 123}))
return (
<ul> {data.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul>
);
}
Copy the code
Through encapsulation, we simplify the business, reduce redundant code, and make logic clearer.
The encapsulation above brings with it several features, dependency requests. When useRequest’s params are updated, useRequest rerequests. – Request status management. Loading status
A commercial product, however, would require more than just caching of data, unified management of apis, and error retries. Typescript projects also need to support the derivation of API parameters, leading to more friendly code hints.
While SWR covers most of the features we need, we will encapsulate them based on SWR. While preserving SWR features, we will add what SWR lacks: – API for unified management – typescript type inference – support for dynamic dependency requests
Api for unified management
When we manage the API, we need to write a lot of boilerplate code to define the API, so we need a generator to simplify it.
import { AxiosRequestConfig } from 'axios'
export type ApiConfig<Params = any, Data = any> = AxiosRequestConfig & {
url: stringmethod? : Method params? : Params data? : Params _response? : Data [x:string] :any
}
export type Service<Params = any, Data = any> = (_params? :any) = > ApiConfig<Params, Data>
export constcreateGetApi = <Params = any, Data = any>(apiConfig: ApiConfig) => { return (params: Params): Service<Params, Data> => (_params = {}) => { const nextParams = { ... params, ... _params } return { ... apiConfig, params: nextParams, method: 'get' } } } export const createPostApi = <Params = any, Data = any>(apiConfig: ApiConfig) => { return (data: Params): Service<Params, Data> => { return () => ({ ... apiConfig, data, method: 'post' }) } }Copy the code
We defined two Api generators based on AXIOS. What the generator does is define the basic configuration of the API and the type definitions for parameters and return values. Let’s use it:
interface GetUserParmas {
id: number
}
interface GetUserResponse {
id: number
userName: string
}
const userModule = {
getUser: createGetApi<GetUserParmas, GetUserResponse>({url: ‘/user’})
}
Copy the code
As you can see, this makes it very easy to define the API. We are using a Custom hook to unify the API
const useApiSelector = () => {
return {
userModule
}
}
Copy the code
Now that we have completed the first step of the unified management Api, we need to encapsulate the SWR to support the features required for appeal:
Encapsulate new features based on SWR
function useAsync<Params = any.Data = any> (Service: ServiceCombin
,
,>) :BaseResult<Params.Data>
function useAsync<Params = any.Data = any> (service: ServiceCombin<Params, Data>, config) {
// swrGlobal configurationconst globalConfig = React.useContext(Provider)
const requestConfig = Object.assign({}, DEFAULT_CONFIG, globalConfig as any, config || {}) // Pass in parametersfetcherAssurance andswrconsistentif (requestConfig.fetcher) {
requestConfig['fetcher'] = fetcherWarpper(requestConfig.fetcher)
}
const configRef = React.useRef(requestConfig)
configRef.current = requestConfig
// Retrieve the API configuration
const serviceConfig = genarateServiceConfig(service)
const serviceRef = React.useRef(serviceConfig)
serviceRef.current = serviceConfig
// Convert API parameters to SWR keys
const serializeKey = React.useMemo(() = > {
if(! serviceRef.current)return null
try {
return JSON.stringify(serviceRef.current)
} catch (error) {
return new Error(error)
}
}, [serviceRef.current])
if (serializeKey instanceof Error) {
invariant(false.'[serializeKey] service must be object')}const response = useSWR<Response<Data>>(serializeKey, configRef.current)
const getParams = React.useMemo(() = > {
if(! serviceRef.current)return undefined
return(serviceRef.current? .params || serviceRef.current? .data || {})as Params
}, [serviceRef.current])
// Validates the return value, which can be ignored
const disasterRecoveryData = React.useMemo(() = > {
returngetDisasterRecoveryData<Data>(response? .data) }, [response.data])return{... response, ... disasterRecoveryData,params: getParams
}
}
Copy the code
Ok, so we’re done wrapping the SWR, deriving response from the API definition type passed in. Let’s compare the current scheme with the previous one:
const App = () = > {
const apis = useApiSelector()
// In typescirpt applications, you can automatically deduce the parameter types required by getUser
const { data, isValidating } = useRequest(api.getUser({id: 1}))
return (
<div>{data}</div>)}Copy the code
Conclusion:
Above, I mainly elaborated my thoughts on the combination of SWR and business. When choosing a data flow management solution, it is not always necessary to choose one that is used by everyone and needs a more appropriate one for your business.
The code above is just a guide. The complete solution has been uploaded to Github, and you can check the complete code in the repository. The complete code contains more features, including antD support for paging and other features.