Skip to content
This repository has been archived by the owner on May 2, 2024. It is now read-only.

Latest commit

 

History

History
183 lines (149 loc) · 5.79 KB

README.md

File metadata and controls

183 lines (149 loc) · 5.79 KB

coredux

Dualism to Redux. Two-way combining of redux modules.

Example: https://github.com/kana-sama/coredux-example

Motivation

Reducer is just a composable first class setter. Also if some reducer changes a value on some action, it will change this value in a more complicated structure after combining multiple reducers into object reducer too. So, we can combine reducers as we please.

But with selectors everything is different , in the selectors we must know the full path to value in the state, so after reducers relocating we should fix some selectors. IMO it is a problem, we can do it better.

Welcome new concept - Node. It is not just setter, like a reducer, and not just getter, like a selector, it is a COMPOSABLE combination of setters and getters. We can combine any nodes and all selectors will automatically know, how to get the path to value, like actions in reducers.

Example

A simple example of the frequent case - table of posts - tuple of ids and normalized entities of posts.

Let's begin with defining initial state:

const defaultState = {
  areFetching: false,
  ids: [],
  entities: new Map(),
};

After we will define actions - commands for updating (setting) data:

export const fetchPostsRequest = createAction();
export const fetchPostsSuccess = createAction();

And queries for selecting (getting) data:

export const getArePostsFetching = createQuery();
export const getPostsIds = createQuery();
export const getPostsEntities = createQuery();
export const getPosts = createQuery();

And the realization of these setters and getters as the node:

export const posts = createNode(defaultState)
  .setter(fetchPostsRequest, state => ({ ...state, areFetching: true }))
  .setter(fetchPostsSuccess, (state, posts) => ({
    areFetching: false,
    ids: posts.map(post => post.id),
    entities: new Map(posts.map(post => [post.id, post])),
  }))
  .getter(getArePostsFetching, select => state => state.areFetching)
  .getter(getPostsIds, select => state => state.ids)
  .getter(getPostsEntities, select => state => state.entities)
  .getter(getPosts, select =>
    createSelector(
      select(getPostsEntities),
      select(getPostsIds),
      (entities, ids) => ids.map(id => entities.get(id))
    )
  );

select - converts queries to selectors.

Now we can use this node for setting and getting data:

const posts = [
  { id: 1, text: "hello", commentsIds: [1] },
  { id: 2, text: "world", commentsIds: [2, 3] },
];

const state1 = defaultState;
const state2 = posts.reducer(state1, fetchPostsRequest());
posts.select(getArePostsFetching)(state2); // true
const state3 = posts.reducer(state2, fetchPostsSuccess(posts);
posts.select(getPostsIds)(state3); // [1, 2]
posts.select(getPosts)(state3); // posts

Let's create another node for comments in another style:

import { createAction, createQuery, createNode, combineNodes } from "coredux";

export const fetchCommentsRequest = createAction();
export const fetchCommentsSuccess = createAction();

export const getAreCommentsFetching = createQuery();
export const getCommentsIds = createQuery();
export const getCommentsEntities = createQuery();

const areFetching = createNode(false)
  .setter(fetchCommentsRequest, true)
  .setter(fetchCommentsSuccess, false)
  .getter(getAreCommentsFetching);

const ids = createNode([])
  .setter(fetchCommentsSuccess, (ids, comments) =>
    comments.map(comment => comment.id)
  )
  .getter(getCommentsIds);

const entities = createNode(new Map())
  .setter(
    fetchCommentsSuccess,
    (entities, comments) =>
      new Map(comments.map(comment => [comment.id, comment]))
  )
  .getter(getCommentsEntities);

export const comments = combineNodes({
  isFetching,
  ids,
  entities,
});

combineNodes creates a new node for an object with all actions and queries of the subnodes, just like a combineReducers for actions.

And now we can combine these two nodes into one root node and define a complex getter:

export const getPostsWithComments = createQuery();

export const root = combineNodes({
  comments,
  posts,
}).getter(getPostsWithComments, select =>
  createSelector(
    select(getPosts),
    select(getCommentsEntities),
    (posts, commentsById) =>
      posts.map(post => ({
        ...post,
        comments: post.commentsIds.map(id => commentsById.get(id)),
      }))
  )
);

Test:

const commentA = { id: 1, text: "comment A" };
const commentB = { id: 2, text: "comment B" };
const commentC = { id: 3, text: "comment C" };

const comments = [commentA, commentB, commentC];

const postA = { id: 1, text: "post", commentsIds: [commentA.id, commentB.id] };
const postB = { id: 2, text: "post 2", commentsIds: [commentC.id] };

const posts = [postA, postB];

const { dispatch, getState } = createStore(root.reducer);

expect(root.select(getAreCommentsFetching)(getState())).toBe(false);
dispatch(fetchCommentsRequest());
expect(root.select(getAreCommentsFetching)(getState())).toBe(true);
dispatch(fetchCommentsSuccess(comments));
expect(root.select(getAreCommentsFetching)(getState())).toBe(false);

expect(root.select(getCommentsIds)(getState())).toEqual([
  commentA.id,
  commentB.id,
  commentC.id,
]);

const commentsValues = root
  .select(getCommentsEntities)(getState())
  .values();

expect(commentsValues).toContain(commentA);

expect(root.select(getArePostsFetching)(getState())).toBe(false);
dispatch(fetchPostsRequest());
expect(root.select(getArePostsFetching)(getState())).toBe(true);
dispatch(fetchPostsSuccess(posts));
expect(root.select(getArePostsFetching)(getState())).toBe(false);

expect(root.select(getPosts)(getState())).toContain(postA);
expect(root.select(getPostsWithComments)(getState())).toContainEqual({
  ...postA,
  comments: [commentA, commentB],
});