Giter Site home page Giter Site logo

unihooks's Introduction

unihooks experimental Build Status

Essential hooks collection for everyday react1 projects.

NPM

Principles

1. Framework agnostic

Unihooks are not bound to react and work with any hooks-enabled framework:

See any-hooks for the full list.

2. Unified

Unihooks follow useState signature for intuitivity.

let [ state, actions ] = useValue( target?, init | update? )
3. Essential

Unihooks deliver value in reactive context, they're not mere wrappers for native API. Static hooks are avoided.

const MyComponent = () => { let ua = useUserAgent() } // ✘ − user agent never changes
const MyComponent = () => { let ua = navigator.userAgent } // ✔ − direct API must be used instead

Hooks

useChannel

[value, setValue] = useChannel(key, init?, deps?)

Global value provider - useState with value identified globally by key. Can be used as value store, eg. as application model layer without persistency. Also can be used for intercomponent communication.

init can be a value or a function, and (re)applies if the key (or deps) changes.

import { useChannel } from 'unihooks'

function Component () {
  let [users, setUsers] = useChannel('users', {
    data: [],
    loading: false,
    current: null
  })

  setUsers({ ...users, loading: true })

  // or as reducer
  setUsers(users => { ...users, loading: false })
}
useStorage

[value, setValue] = useStorage(key, init?, options?)

useChannel with persistency to local/session storage. Subscribes to storage event - updates if storage is changed from another tab.

import { useStorage } from 'unihooks'

function Component1 () {
  const [count, setCount] = useStorage('my-count', 1)
}

function Component2 () {
  const [count, setCount] = useStorage('my-count')
  // count === 1

  setCount(2)
  // (↑ updates Component1 too)
}

function Component3 () {
  const [count, setCount] = useStorage('another-count', (value) => {
    // ...initialize value from store
    return value
  })
}

options

  • prefix - prefix that's added to stored keys.
  • storage - manually pass session/local/etc storage.

Reference: useStore.

useAction

[action] = useAction(key, cb, deps?)

Similar to useChannel, but used for storing functions. Different from useChannel in the same way the useCallback is different from useMemo. deps indicate if value must be reinitialized.

function RootComponent() {
  useAction('load-content', async (slug, fresh = false) => {
    const url = `/content/${slug}`
    const cache = fresh ? 'reload' : 'force-cache'
    const res = await fetch(url, { cache })
    return await res.text()
  })
}

function Content ({ slug = '/' }) {
  let [content, setContent] = useState()
  let [load] = useAction('load-content')
  useEffect(() => load().then(setContent), [slug])
  return html`
    <article>${content}</article>
  `
}
useSearchParam

[value, setValue] = useSearchParam(name, init?)

Reflect value to location.search. value is turned to string via URLSearchParams. To serialize objects or arrays, provide .toString method or convert manually.

NOTE. Patches history.push and history.replace to enable pushstate and replacestate events.

function MyComponent () {
  let [id, setId] = useSearchParam('id')
}
useCountdown

[n, reset] = useCountdown(startValue, interval=1000 | schedule?)

Countdown value from startValue down to 0 with indicated interval in ms. Alternatively, a scheduler function can be passed as schedule argument, that can be eg. worker-timers-based implementation.

import { useCountdown } from 'unihooks'
import { setInterval, clearInterval } from 'worker-timers'

const Demo = () => {
  const [count, reset] = useCountdown(30, fn => {
    let id = setInterval(fn, 1000)
    return () => clearInterval(id)
  });

  return `Remains: ${count}s`
};
useValidate

[error, validate] = useValidate(validator: Function | Array, init? )

Provides validation functionality.

  • validator is a function or an array of functions value => error | true ?.
  • init is optional initial value to validate.
function MyComponent () {
  let [usernameError, validateUsername] = useValidate([
    value => !value ? 'Username is required' : true,
    value => value.length < 2 ? 'Username must be at least 2 chars long' : true
  ])

  return <>
    <input onChange={e => validateUsername(e.target.value) && handleInputChange(e) } {...inputProps}/>
    { usernameError }
  </>
}
useFormField

[props, field] = useFormField( options )

Form field state controller. Handles input state and validation. Useful for organizing controlled inputs or forms, a nice minimal replacement to form hooks libraries.

let [props, field] = useFormField({
  name: 'password',
  type: 'password',
  validate: value => !!value
})

// to set new input value
useEffect(() => field.set(newValue))

return <input {...props} />

options

  • value - initial input value.
  • persist = false - persist input state between sessions.
  • validate - custom validator for input, modifies field.error. See useValidate.
  • required - if value must not be empty.
  • ...props - the rest of props is passed to props

field

  • value - current input value.
  • error - current validation error. Revalidates on blur, null on focus.
  • valid: bool - is valid value, revalidates on blur.
  • focus: bool - if input is focused.
  • touched: bool - if input was focused.
  • set(value) - set input value.
  • reset() - reset form state to initial.
  • validate(value) - force-validate input.
useInput

[value, setValue] = useInput( element | ref )

Uncontrolled input element hook. Updates if input value changes. Setting null / undefined removes attribute from element. Useful for organizing simple input controllers, for advanced cases see useFormField.

function MyButton() {
  let ref = useRef()
  let [value, setValue] = useInput(ref)

  useEffect(() => {
    // side-effect when value changes
  }, [value])

  return <input ref={ref} />
}
useObservable

[state, setState] = useObservable(observable)

Observable as hook. Plug in any spect/v, observable, mutant, observ be free.

import { v } from 'spect/v'

const vCount = v(0)

function MyComponent () {
  let [count, setCount] = useObservable(vCount)

  useEffect(() => {
    let id = setInterval(() => setCount(count++), 1000)
    return () => clearInterval(id)
  }, [])

  return <>Count: { count }</>
}
standard

For convenience, unihooks export current framework hooks. To switch hooks, use setHooks - the default export.

import setHooks, { useState, useEffect } from 'unihooks'
import * as hooks from 'preact/hooks'

setHooks(hooks)

function Timer() {
  let [count, setCount] = useState(0)
  useEffect(() => {
    let id = setInterval(() => setCount(c => ++c))
    return () => clearInterval(id)
  }, [])
}
utility

Utility hooks, useful for high-order-hooks.

update = useUpdate()

Force-update component, regardless of internal state.

prev = usePrevious(value)

Returns the previous state as described in the React hooks FAQ.

See also

  • any-hooks - cross-framework standard hooks provider.
  • enhook - run hooks in regular functions.

Alternatives

License

MIT

HK

unihooks's People

Contributors

dependabot[bot] avatar dy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

unihooks's Issues

Plural hooks

Multivalue/combining hooks are logically different types of data source.

  • useElements (vs useElement)
  • useFormFields
  • useProperties
  • useAttributes
  • useProps

useState / useEffect async

Make useState support async init, to avoid

let [value,setValue] = useState()
useEffect(async () => {
  setValue()
}, [])

useRender

Although that may seem tempting to turn JSX into hook side-effect, that is not suited for spect case.
Since spect observes elements, the natural main effect (returned result) must be the HTML, replacing the element - that safely turns spect into html reducer.

useStream

let [ value, pull ] = useReadableStream()
let [ , push ] = useWritableStream()

useStore

High-level store/persistency provider.

let [ value, setValue ] = useStore(key, initialValue, { persist: true|'localStorage'|'queryString'|'attribute'|fn })

useComponent / useAPI / useChannel?

Inspired by useFormField, which is controller of some part of DOM, same time the component itself is controller too.

useComponent takes some element (at least), or some reference to component instance, returns state and actions.

let [state, actions] = useComponent(ref)

this logically binds current component function to any state updates of source component.
Therefore component should be able to export state and actions. React exports view, not state/actions.

With useState, useAction we export parts of the API, defined by some component.
With useComponent we can export state/actions together.

// create component exports
function Cart(){
  useComponent('cart', [state, actions])
}

// use component from outside
function Comp() {
  let [state, actions] = useComponent('cart')
}

1. That is very much similar to just useChannel(name, [state, actions]).

2. useStore(key, fn) === useAction === customizable useChannel.

3. Hook name is just custom lens/controller/provider for a value.

It is value with additional logic.

At the core is value and its change. Reactions subscribe to various values in various places: dom elements, model, function, anything - simple subscriptions with fancy syntax. Instead of use we might as well use subscribe(channel, init?)

server context

Used in context of web-server, it can provide hooks for a path handler.

function handler (req) {
  let [{ headers }] = useRequest('./some/path')

  return resp
}

useAction

Some points to reform useAction hook.

  1. useAction is very similar to useCallback - in the same way as useStore is named extension of useState, the useAction is named extension of useCallback.
  2. actions are not necessary to be hooks enabled. All that is required for an action can be defined in (main) component, initializing that action.
  3. 2. allows getting rid of createAction (which now makes actions complex - passive mode etc).

heresy

Test against heresy.
Possibly separate entries would make sense:

import useCountdown from '@unihooks/useCountdown/heresy'

useHook

Anonymizes input [conditioned] hook:

useHook(a ? useA : useB)()

Conceptual discrepancy with react hooks / useArguments === props

React hooks are enclosed - only component itself can trigger own update. Hooks don't subscribe component to external events. The only subscription, triggering component rerendering is props.

unihooks (as well as many other hooks libs) break that convention and subscribe to external data sources. First of all breaking that redux - useSelector literally makes component react on store changes, circumventing props, drawing props in general case a rudiment, not required by reactive function. Literally props mean useArguments, with subscription to function call with new args.

Polyfilled standard hooks

useState(init?, deps?)

  1. Normalizes initializer function (some hook providers have it not implemented).
  2. Takes optional deps to reinitialize state.
function MyComponent(props) {
  // sets `currentValue` to `value` whenever passed `props.value` changes.
  let [currentValue, setCurrentValue] = useState(props.value, [props.value])
}

useEffect(fn, deps?)

  1. Guarantees microtask - react/preact unpredictably call as microtask or sync.
  2. No-deps useEffect(fn) is the same as empty-deps useEffect(fn, []).
    1. React's useEffect(fn) is equivalent to queueMicrotask(fn), which is redundant hook (principle 3).
    2. That is compatible with useState(initFn) (principle 2).
    3. Single-run useEffect(fn) is equivalent to useInit(fn)/useMount(fn) − that reduces cognitive load / lib size (principle 1).
  3. Supports async functions.
  4. Ignores non-functional returns.
function MyComponent(props) {
  let [result, setResult] = useState()

  // called once on init
  useEffect(async () => setResult(await load('/data')))

  // ...
}

Arguments against

That's matter of diversity vs unification.

  • useEffect(() => () => {}) can be useful to destroy/reinit effect, that is not the same as queueMicrotask.
  • useState() can be replaced with useMemo(calc, deps) to provide deps.
  • useInit() is something closer to sync effect, rather than microtask.

2.0

Criticism

  • useProperty is useless practice - instead of observing some external object, which is error-prone, better create and manage own useStore.
  • useLocalStorage, useSessionStorage, useCookie - just particularities of useStore.
  • useGlobalCache - useless.
  • useCookie - does not observe cookies changes.
  • useAttribute - doubtful, props/useElement give better ground here.
  • useElement - false, it does not create selector observer.
  • useSyncEffect - likely unneeded.

In result, stable hooks are:

  • useStore as global named extension of useState
  • useAction as global named extension of useCallback
  • useQueryParam (particularity of useStore, but can be useful for UI)
  • useFormField (in fact - controller of state/actions)
  • useElement (or rather derivatives like useMeta, useTitle etc)

useRemote

react-query

function Todos() {
  const { data, isLoading, error } = useQuery('todos',async () => {const res = await fetch(...args)
  return await res.json()})

  return (
    <div>
      {isLoading ? (
        <span>Loading...</span>
      ) : error ? (
        <span>Error: {error.message}</span>
      ) : data ? (
        <ul>
          {data.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      ) : null}
    </div>
  )
}

zeit/swr

function Profile () {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

use-request

// User Profile component
function UserProfile(props) {
  const [profile, getProfile] = useResource(id => ({
    url: `/user/${id}`,
    method: 'GET'
  }))

  useEffect(() => getProfile(props.userId), [])

  if(profile.isLoading) return <Spinner />

  return (
    <ProfileScreen
      avatar={profile.data.avatar}
      email={profile.data.email}
      name={profile.data.name} />
  )
}

use-api

export const Main = () => {
  const [data, { loading, error }, request] = useApi({
    url: '/api/foo/bar'
  })

  return (
    <>
      {loading && <div>Loading...</div>}
      {error && <div>{error.response.data.errMsg}</div>}
      {data && (
        <>
          <div>Hello! {data.username}</div>
          <button onClick={request}>Reload</button>
        </>
      )}
    </>
  )
}

use-abortable-fetch

const ChuckNorrisJoke = () => {
  const { data, loading, error, abort } = useAbortableFetch(
    '//api.icndb.com/jokes/random/?limitTo=[nerdy]&escape=javascript'
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return null;

  return <div>Joke: {data.value.joke}</div>;
};

use-http

function App () {
  const { loading, error, data } = useFetch('https://example.com/todos', options)
  // const [request, response] = useFetch('...')

  return (
    <>
      {error && 'Error!'}
      {loading && 'Loading...'}
      {data.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
    </>
  )
}

use-request

function App () {
  const [{ loading, error, data }, fetch] = useRequest(getFooApi)

  useEffect(() => { fetch() }, [fetch])

  if (loading) return 'loading'
  if (error) return 'error'

  return data && <div>{data}</div>
}

useInput

Must useInput(name) create ref, if no corresponding element found?

let [ref, setValue] = useInput(name)

Nope, value must be first, ref is an argument.

But what if combine value with input?

let [{ value, ref, error, disabled }, { disable, validate, set, get, reset }] = useInput(name, default)
// or better keep convention
let [value, { ref, error, disabled, disable, validate, set, get, reset }] = useInput(name, default)
let { value, ref, error, disabled, disable, validate, set, get, reset } = useInput(name, default)

Upgrade useStore to useReducer-like API?

Too often it is more useful to have in-place reducers/modifiers, rather than abstract actions.
Something like

let [store, { set, load, ... }] = useStore(initialState, actions)

But then actions may need to be rather reducers-like.

const devices = createStore('devices', {
  items: [],
  id: {},
  loading: false
}, {
  load: function * () {
    yield ({id: {}, loading: true, items: []})
    let result = await api.get(`/devices`)
    yield { items: result.payload, loading: false }
  },
  current: null
})

That creates following pattern, considering flowponents example as well.
Reactive generators can be applied to any objects as reducers - to DOM-objects as morphdom reducers, to store as store reducers, etc.

let devices = {}
createReducer(devices, devices => {
yield {...devices, a,b,c}
await ...load()
yield {...devices, d,e,f}
})

That's not far from just action though.

useDate

Live binding to current date value

Hooks list

Initially https://github.com/unihooks/org/issues/3

Data hooks

  • usePrevious
  • useProperty
  • useState
  • useQueryParam
  • useQuery
  • useStore
  • useAction (similar to useProperties - exposed to current element)
  • useRoute
  • useLocalStorage
  • useRemote
  • useStream
  • useObservable
  • useFiles

DOM hooks

  • useCookie
  • useEvent
  • useElement
  • useAttribute
  • useProperty
  • useLocation
  • useData
  • useClass
  • useForm
  • useRender
  • useMount
  • useStyle
  • useStyleSheet
  • useHost
  • useMutation

Flow hooks

  • useEffect
  • useTimeout
  • useRaf
  • useIdle
  • useInterval
  • useGenerator
  • usePromise
  • useTransition
  • useList
  • useThrottle
  • usePing
  • useToggle
  • useFSM
  • useAsync
  • useHooked - run hooks-enabled effect
  • useAnimate

Interaction hooks

  • useHover
  • useEvent
  • useResize
  • useIntersects
  • useDrag
  • useMove
  • usePan
  • useZoom
  • useKeyPress
  • useShortcut
  • useArrows
  • useFocusOutside

Hardware hooks

  • useNetwork
  • useMedia
  • useAccelerometer

API principles

1. Hook must be reactive

and related to triggering rerendering. No utility functions are allowed.

2. Signature

Tends to be let [state, actions] = useDomain(params)

// since here render is concerning view
let [model, controllers] = useDomain(params)
let [fields, actions] = useForm()

It is derived from [state, setState] = useState(init)

3. Cross-framework

Hooks use any-hook in the base.

useResource

Can be useful to indicate loading state.
That is incurred by gotcha from wishbox - we need to initialize cart with experiences, after the experiences are loaded. For that matter we could wait for experiences condition in reactive aspect, and once they're loaded - that'd trigger initialization.

let [data, {loading}] = useResource(someSource)

That can be akin to useAsyncSource

useEffect group (possible effects)

useMediaEffect(fn, mediaCondition)
useToggleEffect(fn, boolean)
useFSMEffect(states, value)
useEffect(fn, deps, scheduler?)

Technically can be decomposed to useState + regular useEffect

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.