All Projects → teefouad → Speedux

teefouad / Speedux

Licence: mit
Speedux is an opinionated library that makes Redux much more fun to use.

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Speedux

react-pits
React 中的坑
Stars: ✭ 29 (-64.63%)
Mutual labels:  flux, react-redux
React Music
react开发app版的网易云音乐
Stars: ✭ 79 (-3.66%)
Mutual labels:  react-redux
Matplotlib Chord Diagram
plot chord diagram with matplotlib
Stars: ✭ 60 (-26.83%)
Mutual labels:  flux
Reactive Flux
Fluxish model implemented with RxJS
Stars: ✭ 71 (-13.41%)
Mutual labels:  flux
Choerodon Front
Choerodon Front is a total front-end of Choerodon that combines Choerodon IAM and Choerodon DevOps.
Stars: ✭ 62 (-24.39%)
Mutual labels:  react-redux
Realm React Redux
A redux like store with Realm as the state
Stars: ✭ 72 (-12.2%)
Mutual labels:  react-redux
Nanoflux
A very lightweight and dependency-free Flux implementation
Stars: ✭ 56 (-31.71%)
Mutual labels:  flux
Fluxxan
Fluxxan is an Android implementation of the Flux Architecture that combines concepts from both Fluxxor and Redux.
Stars: ✭ 80 (-2.44%)
Mutual labels:  flux
Material Flux
No magic flux implementation library.
Stars: ✭ 76 (-7.32%)
Mutual labels:  flux
React Realization
some simple Realizations of react
Stars: ✭ 71 (-13.41%)
Mutual labels:  react-redux
Dva React Worms
dva新手综合教程
Stars: ✭ 70 (-14.63%)
Mutual labels:  react-redux
Starter React Flux
Generate your React PWA project with TypeScript or JavaScript
Stars: ✭ 65 (-20.73%)
Mutual labels:  flux
Microsoft Graph Explorer V4
React/Redux version of Graph Explorer used to learn the Microsoft Graph Api
Stars: ✭ 74 (-9.76%)
Mutual labels:  react-redux
Dva Starter
完美使用 dva react react-router,最好用的ssr脚手架,服务器渲染最佳实践
Stars: ✭ 60 (-26.83%)
Mutual labels:  react-redux
Webpack React
👍👏react入门,抛砖引玉
Stars: ✭ 79 (-3.66%)
Mutual labels:  react-redux
Simple Universal React Redux
The simplest possible Async Universal React & Redux Boilerplate app, that works on both Mac and Windows
Stars: ✭ 58 (-29.27%)
Mutual labels:  react-redux
Aspnet Starter Kit 2.0
Cross-platform web development with Visual Studio Code, C#, F#, JavaScript, ASP.NET Core, React (ReactJS), Redux, TypeScript. Single-page application boilerplate.
Stars: ✭ 70 (-14.63%)
Mutual labels:  react-redux
Flocks.js
A radically simpler alternative to Flux - opinionated React state and rendering management
Stars: ✭ 72 (-12.2%)
Mutual labels:  flux
Fluxus
Flux for SwiftUI, inspired by Vuex
Stars: ✭ 81 (-1.22%)
Mutual labels:  flux
Suas Android
Unidirectional data flow architecture implementation for Android
Stars: ✭ 80 (-2.44%)
Mutual labels:  flux

Speedux

Version License Downloads Build Status Coveralls GitHub

An opinionated library for managing state in React apps, based on Redux but much easier.

   

Contents

   

Installation

Install with npm

npm install --save speedux

Install with yarn

yarn add speedux

   

Demos

Todos App
Shopping Cart App

   

Quick Tutorial

Using Speedux is pretty easy and straight-forward. First step is to wrap your application in a Provider component and the second step is to use the connect function to connect your components to the store. Normal Redux stuff but with less code.

You can also use createHooks if you prefer to use React hooks.

To understand how it works, let's take an example of a very simple counter app that displays three buttons. One button increases the count on click, another button decreases the count and a third button would reset the count.

1. Wrap your app

Start with the application entry file, it's usually the src/index.js file (assuming create-react-app). You would only need to import the Provider component from Speedux and wrap your application with it.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'speedux';

import Counter from './Counter';

const App = (
  <Provider>
    <Counter />
  </Provider>
);

ReactDOM.render(App, document.getElementById('root'));

That's pretty much all you need to do here.

2. Connect your component

We want the Counter component state to contain a count property with an initial value of zero. To update the count property, we will use three actions: increaseCount, decreaseCount and resetCount.

Connect using connect()

Import the connect function from Speedux and pass it two parameters, the first parameter is your component definition and the second parameter is a configuration object that defines the initial state for your connected component and all the logic required to update it.

import React from 'react';
import { connect } from 'speedux'; 

const Counter = ({ state, actions }) => (
  <div>
    <h1>Count is: { state.count }</h1>

    <button onClick={actions.increaseCount}>
      Increase count
    </button>

    <button onClick={actions.decreaseCount}>
      Decrease count
    </button>

    <button onClick={actions.resetCount}>
      Reset count
    </button>
  </div>
);

export default connect(Counter, {
  name: 'counter',
  
  state: { count: 0 },
  
  actions: {
    increaseCount: () => (currentState) => ({
      count: currentState.count + 1,
    }),
    decreaseCount: () => (currentState) => ({
      count: currentState.count - 1,
    }),
    resetCount: () => ({
      count: 0,
    }),
  },
});

Connect using hooks

Import the createHooks function from Speedux and pass it a string as a single parameter. This function returns a set of React hooks that you can use to read and manipulate the global state of the component.

import React from 'react';
import { createHooks } from 'speedux'; 

const { useState, useActions } = createHooks('counter');

const Counter = () => {
  const state = useState({ count: 0 });
  const actions = useActions({
    increaseCount: () => (currentState) => ({
      count: currentState.count + 1,
    }),
    decreaseCount: () => (currentState) => ({
      count: currentState.count - 1,
    }),
    resetCount: () => ({
      count: 0,
    }),
  });
  
  return (
    <div>
      <h1>Count is: { state.count }</h1>

      <button onClick={actions.increaseCount}>
        Increase count
      </button>

      <button onClick={actions.decreaseCount}>
        Decrease count
      </button>

      <button onClick={actions.resetCount}>
        Reset count
      </button>
    </div>
  );
};

export default Counter;

That's it! You have a fully working counter component that is connected to the Redux store.

The connect function automatically injected the state and actions props into the component props.

This was a very simple example to get you started. Next, you can learn more about the configuration object or keep reading to learn how to create asyncronous actions and listen to actions dispatched by other components.

   

Asyncronous Actions

In a real world application, you might need to fetch data from a remote source and update the UI accordingly. For such cases, you can use an asyncronous action. To create an asyncronous action, simply use a generator function instead of a normal function.

Whenever your generator function yields an object, that object will be used to update the component state in the Redux store. If your generator function yields a Promise object, the function execution will pause until that promise is resolved and the result will be passed to the generator function on the next call.

You can also return an array of Promises if you want to execute multiple requests at the same time.

Here is an example:

const config = {
  name: 'dataFetcher',

  state: {
    loading: false,
    data: null,
  },

  actions: {
    * fetchData() {
      // Yield an object to update the state and indicate that
      // the data is being loaded. You can use `props.state.loading`
      // to display a spinner or something similar.
      yield { loading: true };
    
      // Yield a promise to fetch the data
      const response = yield fetch('/api/posts');
      // `fetch` resolves to a promise that needs to be resolved to json
      const data = yield response.json();
      
      // Finally, yield an object to populate the state with the
      // loaded data and indicate that the data has been loaded
      yield {
        loading: false,
        data,
      };
    },
  },
};

   

Handling Errors

To handle errors in an asyncronous action, you can use .catch() or you can check if the resolved response is an instance of Error:

const config = {
  name: 'faultyDataFetcher',

  state: {
    loading: false,
    data: null,
    error: null,
  },

  actions: {
    /* Handle errors using `.catch()` */
    * fetchData() {
      // Yield an object to update the state and indicate that
      // the data is being loaded. You can use `props.state.loading`
      // to display a spinner or something similar.
      yield { loading: true };
      
      // Yield a promise to fetch the data
      const response = yield fetch('/api/posts').catch(e => {
        console.log('Failed to fetch data');

        // Optionally return a fallback value
        return { failed: true, posts: [] };
      });
      
      // Handle loading errors
      if (response.failed === true) {
        ...
      } else {
        ...
      }
    },

    /* Handle errors using `instanceof` */
    * fetchOtherData() {
      // Yield an object to update the state and indicate that
      // the data is being loaded.
      yield { loading: true };
      
      // Yield a promise to fetch the data
      const response = yield fetch('/api/posts');
      
      // Handle loading errors
      if (response instanceof Error) {
        yield { error: response.message };
      } else {
        ...
      }
    },
  },
};

You can also use the handy function this.isError(response) instead of response instanceof Error.

   

Listening to Actions

You can use the handlers configuration option to listen to any action dispatched by the Redux store.

Simply, use the action type as the key and the handler function as the value. The handler function will always receive the action object as a single parameter and should return an object that specifies the state keys that need to be updated and their new values.

Here is an example:

const config = {
  name: 'routerSpy',

  state: { currentPath: null },

  handlers: {
    '@@router/LOCATION_CHANGE': (action) => {
      return {
        currentPath: action.payload.location.pathname,
      };
    },
  },
};

You can also listen to actions that were defined in a configuration object of another connected component.

For example, if we have a connected component Foo:

export default connect(Foo, {
  name: 'foo',

  actions: {
    saySomething(message) { ... }
  },
  ...
});

And another connected component Baz that needs to listen to action saySomething which would be dispatched by component Foo:

export default connect(Baz, {
  name: 'baz',

  state: {
    text: null,
  },

  handlers: {
    'foo.saySomething': function(action) {
      return {
        text: `Foo said: ${action.payload.message}!`
      };
    },
  },
  ...
});

   

Dispatching Actions

The connect function automatically injects a dispatch function into the component props. You can use the dispatch function to dispatch any action and specify its payload as well.

Here is an example:

import React from 'react';
import { connect } from 'speedux';

const MyComponent = ({ dispatch }) => {
  // Dispatches an action with type 'something' with the specified
  // object as the payload
  function onClickButton() {
    dispatch({
      type: 'someAction',
      payload: {
        value: 'abc',
      },
    });
  }
  
  return (
    <div>
      <button onClick={onClickButton}>
        Dispatch an action
      </button>
    </div>
  );
};

export default connect(MyComponent, {...});

You can also dispatch actions that were defined in a configuration object of another connected component.

For example, let's say that we have a component Profile that displays the availability of a user:

export default connect(Profile, {
  name: 'userProfile',

  state: {
    userStatus: 'online',  
  },

  actions: {
    setUserStatus(userStatus) {
      return { userStatus };
    },
  },
  ...
})

And another component Baz that needs to trigger the setUserStatus action which is defined in the configuration object of component Profile:

const Baz = ({ dispatch }) => {
  function setStatus(status) {
    dispatch('userProfile.setUserStatus', status);
  }
  
  return (
    <div>
      <button onClick={() => setStatus('online')}>
        Appear Online
      </button>
      
      <button onClick={() => setStatus('offline')}>
        Appear Offline
      </button>
    </div>
  );
};

export default connect(Baz, {...})

   

Updating the State

Both action and handler functions define how the state should be updated by returning an object. This object specifies the state keys that need to be updated and their new values. In the following example, changeFoo will only update foo in the state with value Bingo while fiz will remain the same.

const MyComponent = ({ state, actions }) => {
  // Before clicking the button: { foo: 'baz', fiz: 'boo' }
  // After clicking the button: { foo: 'Bingo', fiz: 'boo' }
  console.log(state);
  
  return (
    <div>
      <button onClick={actions.changeFoo}>
        Click me
      </button>
    </div>
  );
};

export default connect(MyComponent, {
  name: 'myComponent',

  state: {
    foo: 'baz',
    fiz: 'boo',
  },

  actions: {
    changeFoo() {
      return { foo: 'Bingo' };
    }
  }
});

Nested State Keys

To update deeply nested state keys, you can use dot notation as a string:

export default connect(MyComponent, {
  name: 'myComponent',

  state: {
    data: {
      list: [
        { props: { name: 'feeb' } },
        { props: { name: 'foo' } },
        { props: { name: 'fiz' } },
      ],
    },
  },
  
  actions: {
    changeFooName(newName) {
      return { 'data.list[1].props.name': newName };
    },
  },
});

Wildcard Character: *

If you would like to modify all items of an array or an object in the state, use a wildcard character:

export default createModule('foo', {
  state: {
    list: [
      { name: 'feeb' },
      { name: 'foo' },
      { name: 'fiz' },
    ],
  },
  
  actions: {
    changeAllNames(newName) {
      return { 'list.*.name': newName };
    },
  },
});

/*
Invoking action changeAllNames('jane') will modify the state to:
{
  list: [
    { name: 'jane' },
    { name: 'jane' },
    { name: 'jane' },
  ],
}
*/

You can also use a wildcard for reading the state as well:

export default connect(MyComponent, {
  name: 'myComponent',

  state: {
    list: [
      { name: 'feeb' },
      { name: 'foo' },
      { name: 'fiz' },
    ],
  },
  
  actions: {
    logAllNames() {
      const names = this.getState('list.*.name');
      console.log(names); // ['feeb', 'foo', 'fiz']
    },
  },
});

Mapper Function

If you need to dynamically calculate the new value of the state key based on the old value, use a mapper function:

export default createModule('foo', {
  state: {
    list: [
      { count: 151 },
      { count: 120 },
      { count: 2 },
    ],
  },
  
  actions: {
    setMinimum() {
      return {
        'list.*.count': (oldValue) => {
          if (oldValue < 50) return 50;
          return oldValue;
        },
      };
    },
  },
});

/*
Invoking action setMinimum() will modify the state to:
{
  list: [
    { count: 151 },
    { count: 120 },
    { count: 50 },
  ],
}
*/

   

Middlewares

To use a middleware, import useMiddleware method and pass it the middleware function. You don't need to use applyMiddleware from Redux, this method will be called internally by Speedux.

Here is an example using React Router DOM (v5.1.2) and Connected React Router (v6.6.1):

import { Provider, useReducer, useMiddleware } from 'speedux';
import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router';
import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

// connected-react-router requires its reducer to be mounted under 'router'
useReducer('router', connectRouter(history));
useMiddleware(routerMiddleware(history));

ReactDOM.render((
  <Provider>
    <ConnectedRouter history={history}>
      ...
    </ConnectedRouter>
  </Provider>
), document.getElementById('root'));

   

API

connect(component, configuration)

Parameter Type Description
component Class | Function Reference to the class/function of the component to be connected to the store.
configuration Object The configuration object for the component.

The connect function connects a component to the Redux store and automatically injects four properties into the component props. These properties are state, actions, globalState and dispatch.

The state prop represents the component state in the Redux store. The default value for the state is an empty object.

The actions prop is a list of action dispatcher functions that correspond to the actions that were defined in the configuration object. The default value for the actions prop is an empty object.

The globalState prop represents the states of other connected components. The default value for the global state is an empty object.

The dispatch prop represents a function that can be used to dispatch any action.

Example:
import React from 'react';
import { connect } from 'speedux';

const MyComponent = ({ state, actions, globalState, dispatch }) => {
  console.log(state); // { value: 'abc' }
  console.log(actions); // { setValue: function(newValue) {...} }
  console.log(globalState); // { foo: 'someValue' }
  console.log(dispatch); // Function

  return <div>...</div>;
};

export default connect(MyComponent, {
  name: 'myComponent',

  state: {
    value: 'abc',
  },

  globalState: {
    foo: 'fooComponent.some.value'
  },

  actions: {
    setValue(newValue) {
      return { value: newValue };
    },
  },
});

 

useReducer(key, reducer)

Allows registering a reducer function that can listen to any action dispatched by the store.

Parameter Type Description
key String A unique identifier key for the reducer.
reducer Function Reducer function to use.
Example:
import { useReducer } from 'speedux';
import { connectRouter } from 'connected-react-router';
import { createBrowserHistory } from 'history';

const history = createBrowserHistory();
const routerReducer = connectRouter(history);

useReducer('router', routerReducer);

 

useMiddleware(middleware)

Allows using middleware functions such as React Router middleware and others. You don't need to use applyMiddleware from Redux before passing the middleware to this function.

Parameter Type Description
middleware Function Middleware function to use.
Example:
import { useMiddleware } from 'speedux';
import { routerMiddleware } from 'connected-react-router';
import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

useMiddleware(routerMiddleware(history));

 

dispatch(actionType, payload)

The dispatch function is automatically injected into the props of a connected component and lets you dispatch any action and specify the action payload as well.

Parameter Type Description
actionType String Type of the action to be dispatched.
payload Object Action payload object.

See Dispatching Actions for an example.

   

The Configuration Object

The configuration object may contain one or more of the following keys:

name (String)

The name key is the only required key in the configuration object. It must be unique for each component as it is used to identify the Redux state and actions for the component.

state (Object)

Represents the component state (or initial state) in the Redux store. If not provided, an empty object will be used as the component initial state.

The component state can only be updated by returning objects from action or handler functions. (explained below)

actions (Object)

A list of all the actions that may need to be dispatched from the component to update the state. Provide the action name as the key and the function as the value.

The key or function name will be used to generate the action type. For example, a name calculator with a defined action addNumbers will dispatch an action of type @@calculator/ADD_NUMBERS whenever props.actions.addNumbers() is called.

The function should return an object that specifies the state keys that need to be updated and their new values.

const config = {
  name: 'calculator',

  state: {
    result: 0,
  },

  actions: {
    addNumbers(x, y) {
      return { result: x + y };
    }
  }
};

To create an asyncronous action, simply use a generator function instead of a normal function.

Whenever your generator function yields an object, that object will be used to update the component state in the Redux store. If your generator function yields a Promise object, the function execution will pause until that promise is resolved and the result will be passed to the generator function on the next call.

See Asyncronous Actions for examples.

handlers (Object)

A list of all the actions that the component needs to listen to and update its state accordingly. Provide the action type as the key and the handler function as the value. You can listen to actions dispatched by other components or any action dispatched by the Redux store.

The handler function will always receive the action object as a single parameter and should return an object that specifies the state keys that need to be updated and their new values.

See Listening to Actions for examples.

globalState (Object)

The globalState key allows reading states of other connected components. Simply provide an object with the name as the key and the state query as the value.

For example, if we have a connected component Cart:

export default connect(Cart, {
  name: 'shoppingCart',

  state: {
    items: [
      { id: 123, price: 12 },
      { id: 456, price: 34 },
      { id: 789, price: 56 },
    ],
    totalCost: 102,
    discountCode: 'u324y32',
  },
  ...
});

And another connected component Checkout that needs to read items inside the state of the Cart component:

const Checkout = ({ globalState }) => {
  console.log(globalState.cartItems); // [{...}, {...}, {...}]
  console.log(globalState.cartItemPrices); // [12, 34, 56]
  ...
};

export default connect(Checkout, {
  name: 'checkout',
  
  globalState: {
    cartItems: 'shoppingCart.items',
    cartItemPrices: 'shoppingCart.items.*.price',
  },
  ...
});

stateKey (String)

The stateKey is used as a property name when the related Redux state object is injected into the component props. The default value is 'state'.

actionsKey (String)

The actionsKey is used as a property name when the action creator functions object is injected into the component props. The default value is 'actions'.

globalStateKey (String)

The globalStateKey is used as a property name when other component states are injected into the component props. The default value is 'globalState'.

dispatchKey (String)

The dispatchKey is used as a property name when the dispatch function is injected into the component props. The default value is 'dispatch'.

   

Hooks

React's hooks let you use state, execute side effects and use other React features without writing a class.

Speedux provides a set of hook APIs as an alternative to the existing connect() Higher Order Component. These APIs allow you to subscribe to the Redux store, handle and dispatch actions, without having to wrap your components in connect().

Here's a quick example to demonstrate how to use them. First, you need to import createHooks function then use it to get access to the other hooks. Remember that the name you pass to createHooks hook must be unique to that component. Alternatively, you can pass a configuration object to createHooks.

import React from 'react';
import { createHooks } from 'speedux';

const {
    useState,
    useActions,
    useHandlers,
    useDispatch,
    useGlobalState,
  } = createHooks('counter');

export default () => {
  const state = useState(...);
  const actions = useActions(...);
  const dispatch = useDispatch();
  const globalState = useGlobalState(...);

  useHandlers(...);
  
  return (
    <div>
      ...
    </div>
  )
}

   

License

MIT

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].