Code Monkey home page Code Monkey logo

redux-symbiote's Introduction

redux-symbiote Build Status Coverage Status All Contributors

Write your actions and reducers without pain

Usage

import { createSymbiote } from 'redux-symbiote'


const initialState = {
  error: null,
  accounts: [],
  loading: false,
}

const symbiotes = {
  accounts: {
    loading: {
      start: (state) => ({ ...state, loading: true }),
      failed: (state, error) => ({ ...state, loading: false, error }),
      finish: (state, accounts) => ({ ...state, loading: false, accounts }),
    },
  },
}

export const { actions, reducer } = createSymbiote(initialState, symbiotes)

Also you can use CommonJS:

const { createSymbiote } = require('redux-symbiote')

// ...

Demo

Edit Redux Symbiote Todos

API

Create symbiote

function createSymbiote(
  initialState,
  symbiotes,
  ?namespace = ''
)

Create action handlers + reducer

createSymbiote(initialState, {
  actionType: actionReducer,
  nestedType: {
    actionType: nestedActionReducer,
  }
})

Example:

const initialState = { value: 1, data: 'another' }

const symbiotes = {
  increment: (state) => ({ ...state, value: state.value + 1 }),
  decrement: (state) => ({ ...state, value: state.value - 1 }),
  setValue: (state, value) => ({ ...state, value }),
  setData: (state, data) => ({ ...state, data }),
  concatData: (state, data) => ({ ...state, data: data + state.data }),
}

export const { actions, reducer } = createSymbiote(initialState, symbiotes)

dispatch(actions.increment()) // { type: 'increment' }
dispatch(actions.setValue(4)) // { type: 'setValue', payload: [4] }
dispatch(actions.decrement()) // { type: 'decrement' }
dispatch(actions.setData('bar')) // { type: 'setData', payload: ['bar'] }
dispatch(actions.concatData('foo ')) // { type: 'concatData', payload: ['foo '] }

// State here { value: 3, data: 'foo bar' }

When you call actions.setValue symbiote calls your action handler with previousState and all arguments spreaded after state.

Nested example

const initialState = { value: 1, data: 'another' }

const symbiotes = {
  value: {
    increment: (state) => ({ ...state, value: state.value + 1 }),
    decrement: (state) => ({ ...state, value: state.value - 1 }),
  },
  data: {
    set: (state, data) => ({ ...state, data }),
    concat: (state, data) => ({ ...state, data: data + state.data }),
  },
}

export const { actions, reducer } = createSymbiote(initialState, symbiotes)

dispatch(actions.value.increment()) // { type: 'value/increment' }
dispatch(actions.value.decrement()) // { type: 'value/decrement' }
dispatch(actions.data.set('bar')) // { type: 'data/set', payload: ['bar'] }
dispatch(actions.data.concat('foo ')) // { type: 'data/concat', payload: ['foo '] }

Options

Third parameter in createSymbiote is optional string or object.

If string passed, symbiote converts it to { namespace: 'string' }.

Object has optional properties:

  • namespace is string — set prefix for each action type
  • defaultReducer is (previousState, action) -> newState — called instead of return previous state
  • separator is string — change separator of nested action types (default /)

ActionHandler##toString

You can use action as action type in classic reducer or in handleAction(s) in redux-actions

import { handleActions } from 'redux-actions'
import { createSymbiote } from 'redux-symbiote'

const initialState = { /* ... */ }

const symbiotes = {
  foo: {
    bar: {
      baz: (state, arg1, arg2) => ({ ...state, data: arg1, atad: arg2 }),
    },
  },
}

const { actions } = createSymbiote(initialState, symbiotes)

const reducer = handleActions({
  [actions.foo.bar.baz]: (state, { payload: [arg1, arg2] }) => ({
    ...state,
    data: arg1,
    atad: arg2,
  }),
}, initialState)

How to use reducer

createSymbiote returns object with actions and reducer.

Created reducer already handles created actions. You don't need to handle actions from symbiote.

// accounts.js
export const { actions, reducer } = createSymbiote(initialState, symbiotes, options)

// reducer.js
import { reducer as accounts } from '../accounts/symbiote'
// another imports

export const reducer = combineReducers({
  accounts,
  // another reducers
})

Why?

Redux recommends creating constants, action creators and reducers separately.

https://redux.js.org/basics/

const ACCOUNTS_LOADING_START = 'ACCOUNTS_LOADING_START'
const ACCOUNTS_LOADING_FAILED = 'ACCOUNTS_LOADING_FAILED'
const ACCOUNTS_LOADING_FINISH = 'ACCOUNTS_LOADING_FINISH'


export function loadingStart() {
  return {
    type: ACCOUNTS_LOADING_START,
  }
}

export function loadingFailed(error) {
  return {
    type: ACCOUNTS_LOADING_FAILED,
    payload: {
      error,
    },
  }
}

export function loadingFinish(accounts) {
  return {
    type: ACCOUNTS_LOADING_FINISH,
    payload: {
      accounts,
    },
  }
}

const initialState = {
  error: null,
  accounts: [],
  loading: false,
}

export function accountsReducer(state = initialState, action) {
  switch (action.type) {
    case ACCOUNTS_LOADING_START:
      return Object.assign({}, state, {
        loading: true,
      })

    case ACCOUNTS_LOADING_FAILED:
      return Object.assign({}, state, {
        loading: false,
        error: action.payload.error,
      })

    case ACCOUNTS_LOADING_FINISH:
      return Object.assign({}, state, {
        loading: false,
        accounts: action.payload.accounts,
      })
  }

  return state
}

So much boilerplate.

Let's look at redux-actions.

import { createActions, handleActions, combineActions } from 'redux-actions'


export const actions = createActions({
  accounts: {
    loading: {
      start: () => ({ loading: true }),
      failed: (error) => ({ loading: false, error }),
      finish: (accounts) => ({ loading: false, accounts }),
    },
  },
}).accounts

const initialState = {
  error: null,
  accounts: [],
  loading: false,
}

export const accountsReducer = handleActions({
  [combineActions(actions.loading.start, actions.loading.failed, actions.loading.finish)]:
    (state, { payload: { loading } }) => ({ ...state, loading }),

  [actions.loading.failed]: (state, { payload: { error } }) => ({ ...state, error }),

  [actions.loading.finish]: (state, { payload: { accounts } }) => ({ ...state, accounts }),
}, initialState)

But we have some duplicate in action creators properties and reducer.

Let's rewrite it to redux-symbiote:

import { createSymbiote } from 'redux-symbiote'

const initialState = {
  error: null,
  accounts: [],
  loading: false,
}

const symbiotes = {
  start: (state) => ({ ...state, loading: true }),
  finish: (state, { accounts }) => ({ ...state, loading: false, accounts }),
  failed: (state, { error }) => ({ ...state, loading: false, error }),
}

export const { actions, reducer: accountsReducer } =
  createSymbiote(initialState, symbiotes, 'accounts/loading')

That's all. accounts/loading is an optional namespace for actions types.

To reduce noise around loading actions try symbiote-fetching.

Contributors

Thanks goes to these wonderful people (emoji key):

Sergey Sova
Sergey Sova

📖 💻 💡 🤔 ⚠️
Arutyunyan Artyom
Arutyunyan Artyom

👀 🤔 🐛 💻
Igor Kamyshev
Igor Kamyshev

📦 ⚠️
Ilya
Ilya

🐛
Ivanov Vadim
Ivanov Vadim

📖
Аnton Krivokhizhin
Аnton Krivokhizhin

📦 🚇
Viacheslav
Viacheslav

🤔 👀
Dmitri Razin
Dmitri Razin

🐛 🎨
Surgie Finesse
Surgie Finesse

💻

This project follows the all-contributors specification. Contributions of any kind welcome!

redux-symbiote's People

Contributors

allcontributors[bot] avatar antonkri97 avatar artalar avatar dependabot[bot] avatar finesse avatar fullstackprincess avatar igorkamyshev avatar ilyaagarkov avatar ivanov-v avatar sergeysova avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  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  avatar  avatar

redux-symbiote's Issues

Types: derive actions type from the symbiotes type

Issuehunt badges

Current TypeScript implementation is not convenient because I have to describe the actions 2 times: the type and the implementation. Moreover, the createSymbiote type doesn't fully check that the actions type and implementation match.

// 1st actions description
interface Actions {
  foo(): Action;
  bar(arg: string): Action
}

// 2nd actions description
const symbiotes = {
  foo(state: State) {
    return {...state, value: 'foo'};
  },
  bar(state: State, value: number) { // The argument type mismatch is not checked
    return {...state, value: `bar ${value}`};
  }
};

const {actions, reducer} = createSymbiote<State, Actions>({value: 'Hi'}, symbiotes);

The single source of truth principle is broken.

I've made a draft of typing for createSymbiote that solves the problem. The function type derives the returned actions type from the input symbiotes type automagically:

export interface NamespaceOptions<State> {
  namespace?: string
  defaultReducer?: (prevState: State, action: Action) => State
  separator?: string
}

export type Symbiote<State> = (state: State, ...payload: any[]) => State

export type Reducer<State> = (state: State, action: Action) => State

export type Actions<Symbiotes> = {
  [Key in keyof Symbiotes]: Symbiotes[Key] extends Function
    ? ActionCreator<Symbiotes[Key]>
    : Actions<Symbiotes[Key]>
}

export interface Action<Payload = any> {
  type: string
  payload?: Payload
}

// Inspired by the built-in Parameters type
type ParametersFromSecond<T> = T extends (_: any, ...args: infer P) => any ? P : never;

type ActionCreator<Symbiote> = (...args: ParametersFromSecond<Symbiote>) => Action;

export function createSymbiote<State, Symbiotes>(
  initialState: State,
  actionsConfig: Symbiotes,
  namespaceOptions?: string | NamespaceOptions<State>,
): {
  actions: Actions<Symbiotes>
  reducer: Reducer<State>
}

The example can be converted with these types:

// Only 1 actions description
const symbiotes = {
  foo(state: State) {
    return {...state, value: 'foo'};
  },
  bar(state: State, value: number) {
    return {...state, value: `bar ${value}`};
  }
};

const {actions, reducer} = createSymbiote({value: 'Hi'}, symbiotes);

actions.bar('cocktail'); // TS error, a number is expected

I can make a PR if you agree. It'll be a breaking change for the typing.


IssueHunt Summary

finesse finesse has been rewarded.

Backers (Total: $5.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

TypeScript

Hello. This lib is great! But, I want use it with TS app. Do you have plans about adding TS support?

Failed to compile with CRA

https://codesandbox.io/s/43m0nvxk34

Steps to reproduce:

  1. Download sources from codesandbox
  2. Install dependencies with npm install
  3. Build npm run build
Creating an optimized production build...
Failed to compile.

Failed to minify the code from this file:

 	./node_modules/redux-symbiote/lib/index.js:2

Read more here: http://bit.ly/2tRViJ9

Nested state

Problem

Many fetch state in a flat state object

Solution

Add support fetch in nested state
For example

  const initial = {
    fetching: {
      main: initialFetching,
      apply: initialFetching,
    },
  }
  const symbiotes = {
    fetchNewsHandler: createFetching("fetching.main"),
    applyNewsHandler: createFetching("fetching.apply"),
  }

[ React Native ] "Your browser does not have secure random generator. If you don’t need unpredictable IDs, you can use nanoid/non-secure."

Issuehunt badges

Hi ! First of all, thanks for all the work you did on redux-symbiote. It's a wonderful lib, writing redux-related code has never been so easy.
I'm developing 2 apps in React Native with Expo, and of course, redux-symbiote.
I upgraded redux-symbiote version from v3.0.2 to v3.1.0 and had this error since :

Your browser does not have secure random generator. If you don’t need unpredictable IDs, you can use nanoid/non-secure.

I tried replacing const nanoid = require("nanoid") with const nanoid = require("nanoid/non-secure") in src/index.js but it didn't do the trick.

My workaround was to rollback to v3.0.2 to get both my apps working again.


IssueHunt Summary

kldkv kldkv has been rewarded.

Backers (Total: $5.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

ToDo

  • Add examples with react, thunk
  • Refactor usage on readme
  • Add guide
  • Add comparison
  • TypeScript typings
  • FlowType typings
  • Draw logo
  • Example on Codesandbox

Store reset

When user logs out, I want to clear the store with one RESET_STORE action.
It's easy to achieve with pure Redux.
Is there an elegant way to reset the whole state to initial values with one action, when using redux-symbiote with namespaces?

TS typings are missing in npm package

Steps to reproduce:

  1. Do npm i redux-symbiote
  2. Open ./node-modules/redux-symbiote
  3. There is no index.d.ts file, so its impossible to use buit-in typings :(
    image

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.