effector

Business logic with ease ☄️

MIT License

Downloads
370.3K
Stars
4.5K
Committers
151
effector - effector 20.12.0

Published by zerobias over 4 years ago

  • Add effect.doneData and effect.failData events with effect result and error values as shorthands for common use cases effect.done.map(({result}) => result) and effect.fail.map(({error}) => error)
import {createEffect, merge} from 'effector'

const fetchUserFx = createEffect()
const fetchFriendsFx = createEffect()

/* create event with results of any request to api */

/* new way, using .doneData events */

const apiResult = merge([
  fetchUserFx.doneData,
  fetchFriendsFx.doneData,
])

/* old way, using .done events */

const apiResultOld = merge([
  fetchUserFx.done.map(({result}) => result),
  fetchFriendsFx.done.map(({result}) => result),
])

Try it

Documentation for effect.doneData

Documentation for effect.failData

effector - effector 20.11.0

Published by zerobias almost 5 years ago

  • Add effect.inFlight store to effects. It show how many effect calls aren't settled yet. Useful for rate limiting.
import {createEffect} from 'effector'

const fx = createEffect({
  handler: () => new Promise(rs => setTimeout(rs, 500)),
})

fx.inFlight.watch(amount => {
  console.log('in-flight requests:', amount)
})
// => 0

const req1 = fx()
// => 1

const req2 = fx()
// => 2

await Promise.all([req1, req2])

// => 1
// => 0

Try it

Documentation for effect.inFlight

  • Introduce withRegion: region-based memory management tool, which attach units (stores, events and effects) and watchers, created inside given callback to lifecycle of owner unit to be erased together with it.
import {createEvent, createDomain, withRegion} from 'effector'

const trigger = createEvent()

const domain = createDomain()

withRegion(domain, () => {
  trigger.watch(n => {
    console.log(n)
  })
})

trigger(0)
// => 0
clearNode(domain)
trigger(1)
// no reaction

Try it

  • Add support for Map<Store, any> to values property in fork.
  • Fix concurrent requests support in effect.pending: it will become false only after all pending effect calls becomes settled.
effector - effector 20.10.0

Published by zerobias almost 5 years ago

  • Add launch({target: unit, params}) overload for launch - low level method for running computation in units (events, effects or stores). Mostly used by library developers for fine-grained control of computations.
effector - effector 20.9.0, effector-react 20.6.0

Published by zerobias almost 5 years ago

  • Introduce effector/fork and effector-react/ssr: api for server side rendering and managing independent instances of application in general.
/**
 * app
 */
import {createDomain, forward, restore} from 'effector'
import {useStore, useList, Provider, useEvent} from 'effector-react/ssr'

export const app = createDomain()

const requestUsername = app.createEffect<{login: string}, string>()
const requestFriends = app.createEffect<string, string[]>()

const username = restore(requestUsername, 'guest')
const friends = restore(requestFriends, [])

forward({
  from: requestUserName.done.map(({result: username}) => username),
  to: requestFriends,
})

const Friends = () => (
  <ul>
    {useList(friends, friend => (
      <li>{name}</li>
    ))}
  </ul>
)

const Title = () => <header>Hello {useStore(username)}</header>

export const View = ({root}) => (
  <Provider value={root}>
    <Title />
    <Friends />
  </Provider>
)

/**
 * client
 */
import ReactDOM from 'react-dom'
import {fork} from 'effector/fork'
import {app, View} from './app'

const clientScope = fork(app, {
  values: window.__initialState__,
})

ReactDOM.hydrate(<View root={clientScope} />, document.getElementById('root'))

/**
 * server
 */
import express from 'express'
import {renderToString} from 'react-dom/server'
import {fork, serialize, allSettled} from 'effector/fork'

import {app, View} from './app'

export const server = express()

server.get('/user/:login', async (req, res) => {
  // clone application
  const scope = fork(app)
  // call requestUsername(req.params) in scope
  // and await all triggered effects
  await allSettled(requestUsername, {
    scope,
    params: req.params, // argument for requestUsername call
  })
  // save all stores in application to plain object
  const data = serialize(scope)
  // render dom content
  const content = renderToString(<View root={scope} />)
  res.send(`
    <body>
      ${content}
      <script>
        window.__initialState__ = ${JSON.stringify(data)};
      </script>
    </body>
  `)
})

This solution requires effector/babel-plugin in babel configuration:

{
  "plugins": ["effector/babel-plugin"]
}

Example application with express
Serverless example

  • Add events created with createApi, stores created with restore and events created with .prepend to domain of given source units
import {createDomain, createApi, restore} from 'effector'
const domain = createDomain()
domain.onCreateStore(store => {
  console.log('store created')
})
domain.onCreateEvent(event => {
  console.log('event created')
})

const position = domain.createStore({x: 0})
// => store created
const {move} = createApi(position, {
  move: ({x}, payload) => ({x: x + payload}),
})
// => event created
const lastMove = restore(move, 0)
// => store created

Try it

effector - effector 20.8.2

Published by zerobias almost 5 years ago

  • Improve combine batching in a few edge cases with nested combine calls
import {createEvent, createStore, combine} from 'effector'

const event = createEvent()
const store = createStore(0).on(event, s => s + 1)

const combined = combine([store, combine([store.map(d => d + 1)])])
combined.watch(e => fn(e))
// => [0, [1]]
event()
// => [1, [2]]

Try it

effector - effector-react 20.5.2

Published by zerobias almost 5 years ago

  • Add ability to infer fn argument types without as const in useStoreMap.
    In effector-react 20.0.3 we introduced an improvement for useStoreMap types, which helps to infer types of fn arguments from keys. And now useStoreMap types improved even more: every item in second argument will have its own type even without as const, out from a box

Type tests

useStoreMap in docs

PR #274 (thanks @abliarsar)

import React from 'react'
import {createStore} from 'effector'
import {useStoreMap} from 'effector-react'

type User = {
  username: string
  email: string
  bio: string
}

const users = createStore<User[]>([
  {
    username: 'alice',
    email: '[email protected]',
    bio: '. . .',
  },
  {
    username: 'bob',
    email: '[email protected]',
    bio: '~/ - /~',
  },
  {
    username: 'carol',
    email: '[email protected]',
    bio: '- - -',
  },
])

export const UserProperty = ({id, field}: {id: number; field: keyof User}) => {
  const value = useStoreMap({
    store: users,
    keys: [id, field],
    fn: (users, [id, field]) => users[id][field] || null,
  })
  return <div>{value}</div>
}
effector - effector 20.8.0

Published by zerobias almost 5 years ago

  • Allow to use objects and arrays with stores in sample source
import {createStore, createEvent, sample, combine} from 'effector'

const trigger = createEvent()
const objectTarget = createEvent()
const arrayTarget = createEvent()

const a = createStore('A')
const b = createStore('B')

sample({
  source: {a, b},
  clock: trigger,
  target: objectTarget,
})

sample({
  source: [a, b],
  clock: trigger,
  target: arrayTarget,
})

objectTarget.watch(obj => {
  console.log('sampled object', obj)
})
arrayTarget.watch(array => {
  console.log('sampled array', array)
})

trigger()
// sampled object {a: 'A', b: 'B'}
// sampled array ['A', 'B']

/* old way to do this: */

sample({
  source: combine({a, b}),
  clock: trigger,
  target: objectTarget,
})

sample({
  source: combine([a, b]),
  clock: trigger,
  target: arrayTarget,
})

Try it

effector - effector-react 20.5.0

Published by zerobias almost 5 years ago

  • Pass props to Gate.open & Gate.close events
import {createGate} from 'effector-react'
const PageMeta = createGate()

PageMeta.open.watch(props => {
  console.log('page meta', props)
})

const App = () => (
  <>
    <PageMeta name="admin page" />
    <div>body</div>
  </>
)
ReactDOM.render(<App />, document.getElementById('root'))
// => page meta {name: 'admin page'}

Try it

effector - effector 20.7.0

Published by zerobias almost 5 years ago

  • Add domain.createStore as alias for domain.store (proposal)
  • Add domain.createEvent as alias for domain.event
  • Add domain.createEffect as alias for domain.effect
  • Add domain.createDomain as alias for domain.domain
effector - effector 20.6.2

Published by zerobias almost 5 years ago

effector - effector 20.6.1, effector-react 20.4.1, effector-vue 20.3.2

Published by zerobias almost 5 years ago

  • Add typescript typings for compat builds
  • Improve built-in source maps
effector - effector 20.6.0

Published by zerobias almost 5 years ago

  • Add support for arrays to forward
import {createEvent, forward} from 'effector'

const firstSource = createEvent()
const secondSource = createEvent()
const firstTarget = createEvent()
const secondTarget = createEvent()

forward({
  from: [firstSource, secondSource],
  to: [firstTarget, secondTarget]
})

firstTarget.watch(e => console.log('first target', e))
secondTarget.watch(e => console.log('second target', e))

firstSource('A')
// => first target A
// => second target A
secondSource('B')
// => first target B
// => second target B

Try it

effector - effector-vue 20.3.0

Published by zerobias almost 5 years ago

  • Add createComponent HOC for TypeScript usage. This HOC provides type-safe properties in vue components.
// component.vue
import {createStore, createApi} from 'effector'
import {createComponent} from 'effector-vue'

const $counter = createStore(0)
const {update} = createApi($counter, {
  update: (_, value: number) => value,
})

export default createComponent(
  {
    name: 'Counter',
    methods: {
      update,
      handleClick() {
        const value = this.$counter + 1 // this.$counter <- number ( typescript tips )
        this.update(value)
      },
    },
  },
  {$counter},
)
effector - effector 20.5.0

Published by zerobias almost 5 years ago

  • Merge createStoreObject to combine to reduce api surface. Wherever createStoreObject was used, it can be replaced with combine
import {createStore, combine, createStoreObject} from 'effector'

const r = createStore(255)
const g = createStore(0)
const b = createStore(255)

const color = combine({r, g, b})
color.watch(console.log)
// => {r: 255, b: 0, b: 255}

const colorOld = createStoreObject({r, g, b})
colorOld.watch(console.log)
// => {r: 255, b: 0, b: 255}

Try it

  • Add ability to use arrays of stores with combine
import {createStore, combine} from 'effector'

const r = createStore(255)
const g = createStore(0)
const b = createStore(255)

const color = combine([r, g, b])
color.watch(console.log)
// => [255, 0, 255]

Try it

effector - effector 20.4.4

Published by zerobias almost 5 years ago

  • Ensure that both effect.done and effect.fail are called before effect.finally watchers, thereby preventing side-effects from interrupting pure computations
effector - effector 20.4.3

Published by zerobias almost 5 years ago

  • Throw expected error in case with sample({clock: undefined})
import {createStore, sample} from 'effector'
sample({
  source: createStore(null),
  clock: undefined,
})
// Throw "config.clock should be defined"
effector - effector 20.4.1

Published by zerobias about 5 years ago

effector - effector-vue 20.2.1

Published by zerobias about 5 years ago

  • Add typescript typings for object shape, introduced in effector-vue 20.2.0
const counter = createStore(0)

new Vue({
  effector: {
    counter, // would create `counter` in template
  },
})
effector - effector 20.4.0

Published by zerobias about 5 years ago

  • Introduce guard: conditional event routing
    Control one dataflow with the help of another: when the condition and the data are in different places, then we can use guard with stores as a filters to trigger events when condition state is true, thereby modulate signals without mixing them
import {createStore, createEffect, createEvent, guard, sample} from 'effector'

const clickRequest = createEvent()
const fetchRequest = createEffect({
  handler: n => new Promise(rs => setTimeout(rs, 2500, n)),
})

const clicks = createStore(0).on(clickRequest, x => x + 1)
const requests = createStore(0).on(fetchRequest, x => x + 1)

const isIdle = fetchRequest.pending.map(pending => !pending)

/*
on clickRequest, take current clicks value,
and call fetchRequest with it
if isIdle value is true 
*/
guard({
  source: sample(clicks, clickRequest),
  filter: isIdle,
  target: fetchRequest,
})

See ui visualization

Also, guard can accept common function predicate as a filter, to drop events before forwarding them to target

import {createEffect, createEvent, guard} from 'effector'

const searchUser = createEffect()
const submitForm = createEvent()

guard({
  source: submitForm,
  filter: user => user.length > 0,
  target: searchUser,
})

submitForm('') // nothing happens
submitForm('alice') // ~> searchUser('alice')

Type inference
Implementation tests

effector - effector 20.3.2

Published by zerobias about 5 years ago

  • Allow typescript to refine type with split method (PR)
  • Improve type inference of effects with optional arguments in Typescript (PR)
  • Ensure that effect handler is called only after effect update itself, thereby preventing side-effects from interrupting pure computations
import React from 'react'
import ReactDOM from 'react-dom'
import {createStore, createEvent, createEffect, sample} from 'effector'
import {useList} from 'effector-react'

const items$ = createStore([{id: 0, status: 'NEW'}, {id: 1, status: 'NEW'}])

const updateItem = createEvent()
const resetItems = createEvent()

const processItems = createEffect({
  async handler(items) {
    for (let {id} of items) {
      //event call inside effect
      //should be applied to items$
      //only after processItems itself
      updateItem({id, status: 'PROCESS'})
      await new Promise(r => setTimeout(r, 3000))
      updateItem({id, status: 'DONE'})
    }
  },
})

items$
  .on(updateItem, (items, {id, status}) =>
    items.map(item => (item.id === id ? {...item, status} : item)),
  )
  .on(processItems, items => items.map(({id}) => ({id, status: 'WAIT'})))
  .reset(resetItems)

const processClicked = createEvent()

sample({
  source: items$,
  clock: processClicked,
  target: processItems,
})

const App = () => (
  <section>
    <header>
      <h1>Jobs list</h1>
    </header>
    <button onClick={processClicked}>run tasks</button>
    <button onClick={resetItems}>reset</button>
    <ol>
      {useList(items$, ({status}) => (
        <li>{status}</li>
      ))}
    </ol>
  </section>
)

ReactDOM.render(<App />, document.getElementById('root'))

Try it

Package Rankings
Top 1.08% on Npmjs.org
Top 14.96% on Deno.land
Badges
Extracted from project README
Tested with browserstack