1.0.5 • Published 2 years ago

@acato/react-utils v1.0.5

Weekly downloads
-
License
ISC
Repository
-
Last release
2 years ago

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 createSharedState can be used anywhere in your app. What I mean by that is that createSharedState 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 [setPartialState, useSharedState, getState] = createSharedState({
      cart: { items: ['prod #1'] },
      todos: { items: ['todo #1'] }
    })
  export { setPartialState, useSharedState, getState }  

Get state (hook):

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

Set state:

const setTodos = setPartialState('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 = getState('todos.items');
     // doSomethingWithTodos(todos)
 }

Derived state:

const myDerivedState = createSelector(data => {
    // data is an array here
    return [...data[0], ...data[1].items]
},['todo.items', 'cart'])

// Usage:
const MyComponent = () => {
    const derived = useSharedState(myDerivedState)
    ...
}

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 = setPartialState('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 { authorization, applyInterceptor, addErrorHandlers } from '@beheeracato/react-utils/axios'
import { promisify } from '@beheeracato/react-utils/utils'

const errorHandler = addErrorHandlers({
  match: 401,
  handler: promisify((errResponse, axios) => {
    // do something with err response?
    // axios is the axios instance
    })
  })
})

export const intercept = applyInterceptor(
    // get and return token from somewhere?),
  authorization(() => getTokenFromSomewhere()),// request interceptor
  errorHandler // response error interceptor
)

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

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

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

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:

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

const [setPartialState, useSharedState, getSharedState] = createSharedState({
  myslice: {
    nested: {}
  },
  anotherSlice: 'yeah!'
})

It returns 3 functions:

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

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

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

Create/use derived state:
createSelector(callbackFn, paths)

The createSelector function allows you to derive state from one or more state slices.

const derivedStateSelector = createSelector(data=>{
  ....
},['path.to.something', 'anotherpath', etc])

Usage:

const myDerivedState = useSharedState(derivedStateSelector)

HTTP/fetch

This module allows you to create Axios based resources (instances), services and custom fetch hooks.

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) => (axiosResource)
Applies interceptors to an Axios resource, created with createResource. returns the Axios instance/resource.

addErrorHandlers(...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.

authorization(callback)
Adds authorization header to the request.(Bearer)

Full example:

const errorHandler = addErrorHandlers({
  match: 401,
  handler: promisify((err, axios) => {
    
   })
  },
  {
    match: /40/,
    handler: (err, axios)=>{

    }
  }
  )
})
export const intercept = applyInterceptor(
  authorization(() => getMyTokenFromSomewhere()),
  errorHandler
)

Usage:

const myResource = intercept(createResource({ baseURL: '...', config}))

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:

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 (...)
  }
1.0.5

2 years ago

1.0.0

2 years ago