/retalk

🐤 The Simplest Redux

Primary LanguageJavaScriptMIT LicenseMIT

Retalk

Simplest solution for Redux, write Redux like React.

Travis Codecov npm version npm bundle size npm downloads license

English | 简体中文


Why

  • Simplest Redux - Same syntax as a React component.
  • Only 3 API - setStore(), withStore(), <Provider>.
  • Async models - Fully code splitting support for models.
  • Auto loading - Auto loading state for async actions.

Install

yarn add retalk

or

npm install retalk

Usage

1. Models

Usually we'll set several routes in our app, one route with one model, so we'll have several models.

Write the model like a React component, just without the lifecycle methods.

class CounterModel {
  state = {
    count: 0,
  };
  increment() {
    // this.state -> Get own model's state
    // this.setState() -> Set own model's state
    // this.someAction() -> Call own model's actions

    // this.models.someModel.state -> Get other models' state
    // this.models.someModel.someAction() -> Call other models' actions

    const { count } = this.state;
    this.setState({ count: count + 1 });
  }
  async incrementAsync() {
    // Auto `someAsyncAction.loading` state can be use

    await new Promise((resolve) => setTimeout(resolve, 1000));
    this.increment();
  }
}

2. Store

Use setStore() to setup all models with theirs namespaces.

import { setStore } from 'retalk';

const store = setStore({
  counter: CounterModel,
  // Other models...
});

3. Views

Use withStore() to connect models and components.

import React from 'react';
import { withStore } from 'retalk';

const Counter = ({ count, increment, incrementAsync }) => (
  <div>
    <p>{count}</p>
    <button onClick={increment}>+</button>
    <button onClick={incrementAsync}>+ Async{incrementAsync.loading && '...'}</button>
  </div>
);

const CounterWrapper = withStore('counter')(Counter);

4. App

Use <Provider> to provide the store to your app.

import ReactDOM from 'react-dom';
import { Provider } from 'retalk';

const App = () => (
  <Provider store={store}>
    <CounterWrapper />
  </Provider>
);

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

Demo

Edit retalk

API

1. setStore()

setStore(models, middleware)

const store = setStore({ a: AModel, b: BModel }, [middleware1, middleware2]);

Setup the one and only store.

In development mode, Redux DevTools will be enabled by default, make sure its version >= v2.15.3 and not v2.16.0.

2. withStore()

withStore(...modelNames)(Component)

const DemoWrapper = withStore('a', 'b')(Demo);

Eject one or more models' state and actions to a component's props.

3. <Provider>

<Provider store={store}>

Wrap your app with it to access the store.

FAQ

1. Async import models?

Setup the store with setStore(), then use ibraries like loadable-components to import components and models.

Then, use store.add(models) to eject the imported models to the store.

Here is an example with loadable-components:

import React from 'react';
import loadable from 'loadable-components';

const AsyncCounter = loadable(async (store) => {
  const [{ default: Counter }, { default: CounterModel }] = await Promise.all([
    import('./Counter/index.jsx'),
    import('./Counter/Model.js'),
  ]);
  store.add({ counter: CounterModel }); // Use `store.add(models)`, like `setStore(models)`
  return (props) => <Counter {...props} />;
});

2. Customize state and actions?

Pass mapStateToProps and mapDispatchToProps to withStore(), when you need to customize the ejected props, without passing models' names to withStore().

const mapState = ({ counter: { count } }) => ({
  count,
});

const mapActions = ({ counter: { increment, incrementAsync } }) => ({
  increment,
  incrementAsync,
});

// First parameter to `mapDispatchToProps` is `dispatch`, `dispatch` is a function,
// but in the `mapActions` above, we treat it like an object.
// Retalk did some tricks here, it's the `dispatch` function, but bound models to it.

export default withStore(mapState, mapActions)(Counter);

3. Support HMR?

Change the entry file index.js to:

const rootElement = document.getElementById('root');
const render = () => ReactDOM.render(<App />, rootElement);

render();

if (module.hot) {
  module.hot.accept('./App', () => {
    render();
  });
}

Make sure that <Provider> is inside the <App> component:

const App = () => (
  <Provider store={store}>
    <Counter />
  </Provider>
);

Migrate from v2 to v3

1. Models

- const counter = {
+ class CounterModel {
-   state: {},
+   state = {};
-   actions: {
-     increment() {
-       const home = this.home; // Get other models
-     },
-   },
+   increment() {
+     const { home } = this.models; // Get other models
+   }
- };
+ }

2. Store

- import { createStore } from 'retalk';
+ import { setStore } from 'retalk';

- const store = createStore({ counter }, { plugins: [logger] });
+ const store = setStore({ counter: CounterModel }, [logger]);

3. Views

- import { connect } from 'react-redux';

- const Counter = ({ incrementAsync, loading }) => (
+ const Counter = ({ incrementAsync }) => (
-   <button onClick={incrementAsync}>+ Async{loading.incrementAsync && '...'}</button>
+   <button onClick={incrementAsync}>+ Async{incrementAsync.loading && '...'}</button>
  );

- const CounterWrapper = connect(...withStore('counter'))(Counter);
+ const CounterWrapper = withStore('counter')(Counter);

4. App

- import { Provider } from 'react-redux';
+ import { Provider } from 'retalk';

License

MIT © nanxiaobei