@acato/react-utils 中文文档教程

发布于 3年前 浏览 16 更新于 3年前

react-utils

基于 axios 资源的状态管理和获取挂钩的实用程序。

Table of contents

Motivation

在构建前端应用程序时,除了 ui、路由等,我们都必须处理远程 api 调用和状态管理。 我对状态和 http(context、redux、mobx、immer 等)的所有不同方法感到有点厌倦,这些方法被认为是(新的)最好的东西。

我想要一些开箱即用的东西,而无需设置、配置或对结构或体系结构考虑太久。 React Context、Mobx、Redux 等都很棒,但它对我不起作用。

基本上,我需要的是一种可以在任何地方和任何地方使用的解决方案,而无需过多考虑架构和无休止的讨论是否应该使用 context、redux 或任何(新的)孩子。

所以,最后我决定使用 rxjs/ramda 作为状态解决方案,使用 axios/custom hooks 作为 http 部分。 状态解决方案的好处在于它恰恰是:状态。 不是商店。 你也可以在 React 之外管理和使用状态。

另一方面,http 部分由拦截器实用程序组成,创建 axios 资源并通过自定义挂钩将这些资源用作服务。

Features

  • State: get and set state inside or outside of React
  • Create Axios based http resources and services
  • Apply request and response interceptors to resources
  • Create custom fetch hooks from services
  • Auto abort/cancel fetch requests
  • Memoize fetch responses

Installing

使用 yarn:

yarn add @acato/react-utils

使用 npm:

npm i @acato/react-utils

Example

State

使用 createRxState 创建的状态可以在您应用的任何地方使用。 我的意思是 createRxState 返回一个钩子,它可以在 React 组件内的任何地方使用,而无需使用提供程序、上下文等。 除此之外,它还返回两个函数:setState/setPartialState/setSharedState 和 getState/getPartialState/getSharedState(名称由您决定)。 这两个函数可以在 React 内部和外部使用。

要获取或设置状态,您需要使用路径,例如“todos.items”。

创建状态:

 const [setRxState, useRxState, getRxState] = createRxState({
      cart: { items: ['prod #1'] },
      todos: { items: ['todo #1'] }
    })
  export { setRxState, useRxState, getRxState }  

获取状态

无需回调(挂钩)获取状态:

const MyTodos = ({...rest}) => {
    const todos = useRxState('todos.items')
    ...
    return todos.map(...)
}

获取状态回调(钩子):

const MyTodos = ({...rest}) => {
    const todos = useRxState(items=> items.map(itm=> itm.toUpperCase()), 'todos.items')
    ...
    return todos.map(...)
}

使用多个“选择器”获取状态:

const MyComponent = ({...rest}) => {
    const [todos, cartTotal] = useRxState((...data)=> [data[0].map(...), data[1].items.reduce(...)], ['todos.items', 'cart'])
    ...
    return ...
}

派生状态:

const getFullName = user => `${user.firstName} ${user.lastName}`
const MyComponent = ({...rest}) => {
    const userFullName = useRxState(getFullName, 'user')
    ...
    return ...
}

设置状态

设置状态:

const setTodos = setRxState('todos.items')
const MyTodoForm = ({...rest}) => {
    ...
    return <button onClick={e=> setTodos(todos=> [...todos, 'new todo'])}>Add a todo</button>
}

获取状态(js):

 const doSomethingInteresting = () =>{
     const todos = getRxState('todos.items');
     // doSomethingWithTodos(todos)
 }

奖励提示:

  • The state can be used locally, shared or both.
  • You can create your own little scoped api for managing a state-slice, like:
    const setCart = setRxState('cart.items')
    const cartApi = {
        addItem: item=> setCart(items=> [...items, item]),
        removeItem: item=> setCart(items => items.filter(...)),
        etc...
    }

HTTP

Resources

createResource 只是一个方便的函数在创建的 axios 实例上设置一些默认标头:

export const createResource = ({ baseURL, config = getDefaultConfig() }) =>
 axios.create({
    baseURL,
    ...config
  })

当然,您也可以传递自己的配置。

    createResource({baseURL: '...', config: {...getDefaultConfig(), ...<your config here>}})

所以现在我们有了资源。 我们能用它做什么?

我们可以应用请求/响应拦截器:

import { applyInterceptor, addHandler } from '@acato/react-utils/axios'
const requestConfig = (config) => {
  config.headers['Authorization'] = `Bearer ${getToken()}`
  return config
}
const responseSuccessConfig = (res) => res
const responseErrorConfig = addHandler(
  {
    match: /502/,
    handler: async(err, axios) => {
      // handle 502 errors
    }
  },
  {
    match: /40/,
    handler: async(err, axios) => {
      // handle 40* errors
    }
  }
)

const intercept = applyInterceptors({
  request: requestConfig,
  response: responseSuccessConfig,
  error: responseErrorConfig
})

我们回到之前创建资源的步骤并使用拦截器“装饰”我们的资源:

export const myResource = intercept(createResource(...))

当然您可以调整您的默认配置:

const interceptWithMultipleHeaders = applyInterceptors({
  request: (config) => {
    config.headers['x-header'] = 'x-value'
    return requestConfig(config)
  },
  response: res=> {
    // do something with the response
  },
  error: responseErrorConfig
})

接下来,我们创建一个服务:

export const myService = {
  getSomething: (signal) => () => myResource.get('api/something', { signal: signal() }),
  postSomething: (signal) => (data) => mockResource.post('api/anything', {..data}, { signal: signal() })
}

您会注意到一个函数带有一个 signal 参数,该参数返回另一个函数,该函数实际上执行 api 调用。 这个信号是一个内部使用的函数,通过我们现在创建的获取挂钩取消/中止请求:

export const myFetchHooks = createFetchHook(myService)

函数 createFetchHook 将为服务对象中的每个键创建一个自定义挂钩。 myFetchHooks 将具有以下属性:

    useGetSomething
    usePostSomething

每个创建的提取挂钩都可以记住结果,前提是它不是错误。

现在我们准备好使用我们新的 fetch hooks 了!

const myComponent = ()=> {
     const [getSomething, loading, something, err, abort] = useGetSomething(
    useMemo(
      () => ({
        memoize: false,
        onError: (err) => console.log('ErrorHandler:', err),
        onSuccess: (res) => {
          const something = res.data.data
          // setState(something)? or do something else?
        }
      }),
      []
    )
  )
  useEffect(()=>{
      return abort
  },[abort])
  useEffect(()=>{
      getSomething()
  },[getSomething])
  // with arguments?
  useEffect(()=>{
      getSomething(arg)
  },[getSomething, arg])
}

API

State module

@acato/react-utils/state

状态模块建立在 rxjs 和 ramda 之上。
它允许消费者在订阅的状态切片发生变化时自动得到通知。

创建(共享)状态:

createRxState(initialValue)
其中 initialValue 理想情况下是对象文字。

const [setRxState, useRxState, getRxState] = createRxState({
  myslice: {
    nested: {}
  },
  anotherSlice: 'yeah!'
})


它返回 3 个函数:

  • a set function for setting state (can be used outside React).
  const setNestedState = setRxState('myslice.nested')  

  setNestedState(currentValue => ({...currentValue, newKey: 'something'}))  

  setNestedState({newKey: 'something'})
  • a hook function for consuming (shared) state.
const myState = useRxState('myslice.nested')  
  • a get function for getting state, outside of React.
const doSomething = (...args) => {
  const nestedState = getRxState('myslice.nested')
  ...
}

HTTP Axios

@acato/react-utils/axios

这个模块允许你创建基于 Axios 的资源(实例)和拦截器。

创建资源:

createResource({ baseURL, config = getDefaultConfig() })
使用默认配置:

  const myResource = createResource({ baseURL: 'http://...'})

使用自定义配置:

  const myResource = createResource({ baseURL: 'http://...', config: 
    getDefaultConfig({'my-header': 'myheaderValue'})   
 })

拦截器:

applyInterceptor( {request, response, error} ) => (axiosResource)
将拦截器应用于使用 createResource 创建的 Axios 资源。 返回 Axios 实例/资源。

addHandler(…处理程序)
将错误处理程序添加到响应错误拦截器。

  • Note that the order of the handlers matter, because the first match will be used to handle the specific error.
  • So to be sure your handler will be executed, add handlers from specific to more generic.

服务:

const myService = {
  getSomething: (signal) => () => myResource.get('api/something', { signal: signal() }),
  postAnything: (signal) => (data) => myResource.post('api/anything',{...data}, { signal: signal() })
}

信号回调函数是允许自动取消未决请求的函数。 它由自定义钩子创建器函数控制。

使用服务/创建钩子:

React fetch hooks

@acato/react-utils/fetch

createFetchHook(serviceObject) 此函数为给定服务对象中的每个键创建一个自定义获取挂钩。

export const myFetchHooks = createFetchHook(myService)

获取钩子的用法:
这个钩子是由上面的 createFetchHook 创建的。

useGetSomething({ memoize = false, onSuccess, onError, onLoadStatus, next }) 参数:

  • memoize, should the response be memoized
  • onSucces: handler that receives the response.
  • onError: handler that receives the error.
  • next: next fetch hook in chain, receives the raw response or the response set by onSuccess handler.

返回一个数组:[serviceMethod, loading, response, error, abort]

  • serviceMethod: the function that does the api call.
  • loading: loading state.
  • response: the response (either set by onSuccess or by the hook itself)
  • error: same for response
  • abort: on unmounting, abort can be used to cancel pending requests.

函数签名对于每个创建的钩子都是相同的!

完整示例:

const [getSomething, loading, response, error, abort] = useGetSomething(
    useMemo(
      () => ({
        memoize: true,
        onError: (err) => {
          // do something with the error
        },
        onSuccess: (res) => {
          const data = res.data.data
          return data
        }
      }),
      []
    )
  )
  // loading will be set automatically
  // 'response' will hold the return value from onSuccess handler.
  // 'error' holds the result of onError handler
  // abort can be used to cancel any pending requests

  useEffect(()=>{
    getSomething() // do the actual api call
  }, [getSomething])

  // abort any pending request on 'unmounting':
  useEffect(() => abort, [abort])

为了进一步改进代码或可重用性,您可以移动到上面自定义挂钩的代码,如下所示:

const { useGetSomething } = createFetchHook(myService)

export const useSomething = ()=>{
const [getSomething, loading, response, error, abort] = useGetSomething(
    useMemo(
      () => ({
        memoize: true,
        onError: (err) => {
          // do something with the error
        },
        onSuccess: (res) => {
          const data = res.data.data
          return data
        }
      }),
      []
    )
  )
}
....
return { something , loading }

在组件中的用法:

  export const MyComponent = ({...args}) => {
    const { something, loading } = useSomething()

    return (...)
  }

react-utils

Utilities for statemanagement and fetch hooks based on axios resources.

Table of contents

Motivation

When building frontend apps we all have to deal with remote api calls and statemanagement, besides ui, routing, etc. I got a bit tired of all different approaches on state and http (context, redux, mobx, immer, etc) that are being considered as the (new) best thing.

I wanted something that works out of the box without having to setup, configure or think too long about structure or architecture. React Context, Mobx, Redux, etc are great but it was just not working for me.

Basically, what I needed was something, a solution that could be used anywhere and everywhere, without overthinking architecture and endless discussions whether or not context, redux or whatever (new) kid on the block should be used.

So, in the end I decided to go with rxjs/ramda for the state solution and axios/custom hooks for the http part. The nice thing about the state solution is that it is precisely that: state. Not a store. And you can manage and use the state outside of React as well.

The http part on the other hand consists of interceptor utilities, creating axios resources and utilizing these as services through custom hooks.

Features

  • State: get and set state inside or outside of React
  • Create Axios based http resources and services
  • Apply request and response interceptors to resources
  • Create custom fetch hooks from services
  • Auto abort/cancel fetch requests
  • Memoize fetch responses

Installing

Using yarn:

yarn add @acato/react-utils

Using npm:

npm i @acato/react-utils

Example

State

The state created with createRxState can be used anywhere in your app. What I mean by that is that createRxState returns a hook which can be used anywhere inside a React component, without having to use providers, contexts, etc. Besides that, it returns two more functions: setState/setPartialState/setSharedState and getState/getPartialState/getSharedState (names are up to you). These two functions can be used inside as well as outside of React.

To get or set state you will need to use a path, like 'todos.items'.

Create state:

 const [setRxState, useRxState, getRxState] = createRxState({
      cart: { items: ['prod #1'] },
      todos: { items: ['todo #1'] }
    })
  export { setRxState, useRxState, getRxState }  

Getting state

Get state without callback (hook):

const MyTodos = ({...rest}) => {
    const todos = useRxState('todos.items')
    ...
    return todos.map(...)
}

Get state with callback (hook):

const MyTodos = ({...rest}) => {
    const todos = useRxState(items=> items.map(itm=> itm.toUpperCase()), 'todos.items')
    ...
    return todos.map(...)
}

Get state with multiple 'selectors':

const MyComponent = ({...rest}) => {
    const [todos, cartTotal] = useRxState((...data)=> [data[0].map(...), data[1].items.reduce(...)], ['todos.items', 'cart'])
    ...
    return ...
}

Derived state:

const getFullName = user => `${user.firstName} ${user.lastName}`
const MyComponent = ({...rest}) => {
    const userFullName = useRxState(getFullName, 'user')
    ...
    return ...
}

Setting state

Set state:

const setTodos = setRxState('todos.items')
const MyTodoForm = ({...rest}) => {
    ...
    return <button onClick={e=> setTodos(todos=> [...todos, 'new todo'])}>Add a todo</button>
}

Get state (js):

 const doSomethingInteresting = () =>{
     const todos = getRxState('todos.items');
     // doSomethingWithTodos(todos)
 }

Bonus tips:

  • The state can be used locally, shared or both.
  • You can create your own little scoped api for managing a state-slice, like:
    const setCart = setRxState('cart.items')
    const cartApi = {
        addItem: item=> setCart(items=> [...items, item]),
        removeItem: item=> setCart(items => items.filter(...)),
        etc...
    }

HTTP

Resources

createResource is just a convenience function to set some default headers on the created axios instance:

export const createResource = ({ baseURL, config = getDefaultConfig() }) =>
 axios.create({
    baseURL,
    ...config
  })

Of course, you can pass your own config as well.

    createResource({baseURL: '...', config: {...getDefaultConfig(), ...<your config here>}})

So now we have a resource. What can we do with it?

We can apply request/response interceptors:

import { applyInterceptor, addHandler } from '@acato/react-utils/axios'
const requestConfig = (config) => {
  config.headers['Authorization'] = `Bearer ${getToken()}`
  return config
}
const responseSuccessConfig = (res) => res
const responseErrorConfig = addHandler(
  {
    match: /502/,
    handler: async(err, axios) => {
      // handle 502 errors
    }
  },
  {
    match: /40/,
    handler: async(err, axios) => {
      // handle 40* errors
    }
  }
)

const intercept = applyInterceptors({
  request: requestConfig,
  response: responseSuccessConfig,
  error: responseErrorConfig
})

We go back to the previous step where we created the resource and 'decorate' our resource with interceptors:

export const myResource = intercept(createResource(...))

Of course you can tweak your default config:

const interceptWithMultipleHeaders = applyInterceptors({
  request: (config) => {
    config.headers['x-header'] = 'x-value'
    return requestConfig(config)
  },
  response: res=> {
    // do something with the response
  },
  error: responseErrorConfig
})

Next, we create a service:

export const myService = {
  getSomething: (signal) => () => myResource.get('api/something', { signal: signal() }),
  postSomething: (signal) => (data) => mockResource.post('api/anything', {..data}, { signal: signal() })
}

You will notice a function with a signal argument which returns another function, that actually does the api call. This signal is a function that is used internally, cancelling/aborting requests, by the fetch hook we will create now:

export const myFetchHooks = createFetchHook(myService)

The function createFetchHook will create a custom hook for each key in your service object. myFetchHooks will have these properties:

    useGetSomething
    usePostSomething

Each created fetch hook can memoize the result, only if it was not an error.

Now we are ready to go and use our new fetch hooks!

const myComponent = ()=> {
     const [getSomething, loading, something, err, abort] = useGetSomething(
    useMemo(
      () => ({
        memoize: false,
        onError: (err) => console.log('ErrorHandler:', err),
        onSuccess: (res) => {
          const something = res.data.data
          // setState(something)? or do something else?
        }
      }),
      []
    )
  )
  useEffect(()=>{
      return abort
  },[abort])
  useEffect(()=>{
      getSomething()
  },[getSomething])
  // with arguments?
  useEffect(()=>{
      getSomething(arg)
  },[getSomething, arg])
}

API

State module

@acato/react-utils/state

The state module is built on top of rxjs and ramda.
It allows consumers to get notified automatically whenever the subscribed state slice changes.

Create (shared) state:

createRxState(initialValue)
where initialValue ideally is an object literal.

const [setRxState, useRxState, getRxState] = createRxState({
  myslice: {
    nested: {}
  },
  anotherSlice: 'yeah!'
})


It returns 3 functions:

  • a set function for setting state (can be used outside React).
  const setNestedState = setRxState('myslice.nested')  

  setNestedState(currentValue => ({...currentValue, newKey: 'something'}))  

  setNestedState({newKey: 'something'})
  • a hook function for consuming (shared) state.
const myState = useRxState('myslice.nested')  
  • a get function for getting state, outside of React.
const doSomething = (...args) => {
  const nestedState = getRxState('myslice.nested')
  ...
}

HTTP Axios

@acato/react-utils/axios

This module allows you to create Axios based resources (instances) and interceptors.

Creating resources:

createResource({ baseURL, config = getDefaultConfig() })
Usage with default config:

  const myResource = createResource({ baseURL: 'http://...'})

Usage with custom config:

  const myResource = createResource({ baseURL: 'http://...', config: 
    getDefaultConfig({'my-header': 'myheaderValue'})   
 })

Interceptors:

applyInterceptor( {request, response, error} ) => (axiosResource)
Applies interceptors to an Axios resource, created with createResource. returns the Axios instance/resource.

addHandler(…handlers)
Adds error handlers to the response error interceptor.

  • Note that the order of the handlers matter, because the first match will be used to handle the specific error.
  • So to be sure your handler will be executed, add handlers from specific to more generic.

Services:

const myService = {
  getSomething: (signal) => () => myResource.get('api/something', { signal: signal() }),
  postAnything: (signal) => (data) => myResource.post('api/anything',{...data}, { signal: signal() })
}

The signal callback function is a function that allows automatic cancellation of pending requests. It is controlled by the custom hook creator function.

Using services/creating hooks:

React fetch hooks

@acato/react-utils/fetch

createFetchHook(serviceObject) This function creates a custom fetch hook for each key in the given service object.

export const myFetchHooks = createFetchHook(myService)

Usage of the fetch hooks:
This hook is created by above createFetchHook.

useGetSomething( { memoize = false, onSuccess, onError, onLoadStatus, next }) Arguments:

  • memoize, should the response be memoized
  • onSucces: handler that receives the response.
  • onError: handler that receives the error.
  • next: next fetch hook in chain, receives the raw response or the response set by onSuccess handler.

Returns an array: [serviceMethod, loading, response, error, abort]

  • serviceMethod: the function that does the api call.
  • loading: loading state.
  • response: the response (either set by onSuccess or by the hook itself)
  • error: same for response
  • abort: on unmounting, abort can be used to cancel pending requests.

The function signature is the same for each created hook!

Full example:

const [getSomething, loading, response, error, abort] = useGetSomething(
    useMemo(
      () => ({
        memoize: true,
        onError: (err) => {
          // do something with the error
        },
        onSuccess: (res) => {
          const data = res.data.data
          return data
        }
      }),
      []
    )
  )
  // loading will be set automatically
  // 'response' will hold the return value from onSuccess handler.
  // 'error' holds the result of onError handler
  // abort can be used to cancel any pending requests

  useEffect(()=>{
    getSomething() // do the actual api call
  }, [getSomething])

  // abort any pending request on 'unmounting':
  useEffect(() => abort, [abort])

To further improve code or re-usablity you could move above code to a custom hook, like this:

const { useGetSomething } = createFetchHook(myService)

export const useSomething = ()=>{
const [getSomething, loading, response, error, abort] = useGetSomething(
    useMemo(
      () => ({
        memoize: true,
        onError: (err) => {
          // do something with the error
        },
        onSuccess: (res) => {
          const data = res.data.data
          return data
        }
      }),
      []
    )
  )
}
....
return { something , loading }

Usage in a component:

  export const MyComponent = ({...args}) => {
    const { something, loading } = useSomething()

    return (...)
  }
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文