Gathering detailed insights and metrics for use-backlash
Gathering detailed insights and metrics for use-backlash
Gathering detailed insights and metrics for use-backlash
Gathering detailed insights and metrics for use-backlash
npm install use-backlash
Typescript
Module System
Node Version
NPM Version
TypeScript (98.6%)
JavaScript (1.4%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
1 Stars
82 Commits
1 Watchers
1 Branches
1 Contributors
Updated on Jul 30, 2023
Latest Version
0.0.32
Package Id
use-backlash@0.0.32
Unpacked Size
67.34 kB
Size
11.15 kB
File Count
13
NPM Version
9.6.7
Node Version
18.17.0
Published on
Aug 15, 2023
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
29
useReducer with effects
npm i use-backlash
This hook is a basic approach to split view/logic/effects in React. It works in StrictMode and is easy to test. It is designed to be framework-agnostic and was tested with react and preact.
This is going to be a Counter.
1import React, { useRef, useState, useEffect, useLayoutEffect } from 'react' 2import { UpdateMap, createBacklash } from 'use-backlash' 3 4// A framework should provide react-like hooks 5const useBacklash = createBacklash({ useRef, useState, useEffect, useLayoutEffect }) 6 7// State can be anything, 8type State = number 9 10// but an Action is always a record of tuples, where the key 11// is the name of an action and value is a list of arguments. 12type Action = { 13 inc: [] 14 dec: [] 15} 16 17// init is a pure (in react terms) function that has no arguments 18// and just returns the initial state wrapped in array. 19const init = () => [0] as const 20 21// Unlike the standard useReducer, update/reducer is not a function with 22// a switch statement inside, it is an object where each key is an action 23// name and each value is a reducer that takes a state, rest action 24// elements (if any) and returns next state wrapped in array. There is 25// a helper UpdateMap type, that checks the shape of update object and 26// makes writing types by hand optional. 27const update: UpdateMap<State, Action> = { 28 inc: (state) => [state + 1], 29 30 dec: (state) => [state - 1] 31} 32 33export const Counter = () => { 34 // In this example useBacklash hook takes init & update functions and 35 // returns a tuple containing state & actions. Note that 'init' & 'update' 36 // arguments of useBacklash is 'initial' and changing these things won't 37 // affect the behavior of the hook. Also the actions object is guaranteed 38 // to remain the same during rerenders just like useReducer's dispatch 39 // function. 40 const [state, actions] = useBacklash(init, update) 41 42 return ( 43 <> 44 <div>{state}</div> 45 <button onClick={actions.inc}>inc</button> 46 <button onClick={actions.dec}>dec</button> 47 </> 48 ) 49}
Passing arguments to init function.
1// Let's change the init function to have a single parameter 2const init = (count: number) => [count] as const 3 4// ... 5 6export const Counter = () => { 7 // Inside useBacklash body init function is called only once, 8 // so it is ok to inline it. 9 const [state, actions] = useBacklash(() => init(5), update) 10 11 // ... 12}
For now useBacklash
was used just as a fancy useReducer
that returns an actions object instead of dispatch function. It doesn't make much sense to use it like this instead of useReducer
. So let's make that counter persistent and see how useBacklash
helps to handle side effects.
1// We are going to use localStorage to store the state of the Counter. 2// Since I/O is a side effect it can not be called directly from the init 3// function. To model the situation 'state is not set yet' State type will 4// be extended with 'loading' string literal. 5type State = 'loading' | number 6 7// Additional action 'loaded' will notify that Counter state is loaded. 8type Action = { 9 loaded: [count: number] 10 inc: [] 11 dec: [] 12} 13 14const key = 'counter_key' 15 16// init and each update property functions return 17// the value of Command type - [State, ...Effect[]] 18export const init = (): Command<State, Action> => [ 19 'loading', 20 // The next function is a side effect that will be called by useBacklash 21 // internally. Here it has single parameter - the same actions object 22 // that is returned from useBacklash call. 23 ({ loaded }) => loaded(Number(localStorage.getItem(key)) || 0) 24 // Additional can be added after the first one 25 // and all of them will run in order. 26] 27 28export const update: UpdateMap<State, Action> = { 29 // The second parameter is a value that was passed to the 'loaded' action 30 // a few lines earlier. 31 loaded: (_, count) => [count], 32 33 inc: (state) => { 34 // If someone manages to call 'inc' before the state is loaded, 35 // just do nothing, that's the normal strategy for this example. 36 if (state === 'loading') { 37 return [state] 38 } 39 40 const next = state + 1 41 42 // Like the init function an update returns a Command 43 return [next, () => localStorage.setItem(key, `${next}`)] 44 }, 45 46 dec: (state) => { 47 if (state === 'loading') { 48 return [state] 49 } 50 51 // This line is the only difference between 'inc' and 'dec'. 52 // Probably I will refactor it someday... 53 const next = state - 1 54 55 return [next, () => localStorage.setItem(key, `${next}`)] 56 } 57} 58 59export const Counter = () => { 60 const [state, actions] = useBacklash(init, update) 61 62 return state === 'loading' ? null : ( 63 <> 64 <div>{state}</div> 65 <button onClick={actions.inc}>inc</button> 66 <button onClick={actions.dec}>dec</button> 67 </> 68 ) 69}
Sample test.
1import { act, renderHook } from '@testing-library/react' 2import { useBacklash } from '../src' 3import { init, update } from '../src/Counter' 4 5describe.only('Counter', () => { 6 test('state should equal 1 after inc', () => { 7 const { result } = renderHook(() => useBacklash(init, update)) 8 9 act(() => { 10 result.current[1].inc() 11 }) 12 13 expect(result.current[0]).toEqual(1) 14 }) 15})
When running this test with jest
in jsdom
test environment everything works as expected. But let's imagine that we don't have access to localStorage
in our test environment. In this case test will fail with error: ReferenceError: localStorage is not defined
. To avoid these kind of errors, useBacklash
has an optional third parameter - injects
. This parameter's value will be passed as a second argument to every effect function.
1 import React from 'react' 2 import { Command, UpdateMap, useBacklash } from '../' 3 4 type State = 'loading' | number 5 6 type Action = { 7 loaded: [count: number] 8 inc: [] 9 dec: [] 10 } 11 12+ type Injects = { 13+ readonly getItem: Storage['getItem'] 14+ readonly setItem: Storage['setItem'] 15+ } 16 17 const key = 'counter_key' 18 19- export const init = (): Command<State, Action> => [ 20+ export const init = (): Command<State, Action, Injects> => [ 21 'loading', 22- ({ loaded }) => loaded(Number(localStorage.getItem(key)) || 0) 23+ ({ loaded }, { getItem }) => loaded(Number(getItem(key)) || 0) 24 ] 25 26- export const update: UpdateMap<State, Action> = { 27+ export const update: UpdateMap<State, Action, Injects> = { 28 loaded: (_, count) => [count], 29 30 inc: (state) => { 31 if (state === 'loading') { 32 return [state] 33 } 34 35 const next = state + 1 36 37- return [next, () => localStorage.setItem(key, `${next}`)] 38+ return [next, (_, { setItem }) => setItem(key, `${next}`)] 39 }, 40 41 dec: (state) => { 42 if (state === 'loading') { 43 return [state] 44 } 45 46 const next = state - 1 47 48- return [next, () => localStorage.setItem(key, `${next}`)] 49+ return [next, (_, { setItem }) => setItem(key, `${next}`)] 50 } 51 } 52 53 export const Counter = () => { 54- const [state, actions] = useBacklash(init, update) 55+ // Updating 'injects' doesn't trigger rerenders, so it is safe to inline it. 56+ const [state, actions] = useBacklash(init, update, { 57+ getItem: ((...args) => localStorage.getItem(...args)) as Storage['getItem'], 58+ setItem: ((...args) => localStorage.setItem(...args)) as Storage['setItem'] 59+ }) 60 61 return state === 'loading' ? null : ( 62 <> 63 <div>{state}</div> 64 <button onClick={actions.inc}>inc</button> 65 <button onClick={actions.dec}>dec</button> 66 </> 67 ) 68}
Now the test can be rewritten with mocked localStorage
:
1test('state should equal 1 after inc', () => { 2 let storage: string | null = null 3 4 const { result } = renderHook(() => 5 useBacklash(init, update, { 6 getItem: (_: string) => storage, 7 setItem: (_: string, value: string) => { 8 storage = `${value}` 9 } 10 }) 11 ) 12 13 act(() => { 14 result.current[1].inc() 15 }) 16 17 expect(result.current[0]).toEqual(1) 18 expect(storage).toEqual('1') 19})
It was developed as a boilerplate-free substitute of ts-elmish project. While it doesn't support effect composition or complex effect creators, it should be easier to grasp and have enough power to handle important parts of the UI-logic for any component.
No vulnerabilities found.
No security vulnerabilities found.