1.4.2 • Published 7 years ago

react-simple-flux v1.4.2

Weekly downloads
1
License
MIT
Repository
github
Last release
7 years ago

React Simple Flux

a simple -200line of code- Facebook Flux pattern implementation, just like Redux !.

it use React's 16.3 > new Contect Api mix it with Event Subscription model,to focus on separation of concerns while building modern complex react applications !

Components

just like redux we have

Action creator

it create an action (simple object with a type) and send it to the reducer.

Reducer

receive an action and return a new version of App State

Connected Component / smart+or-dump components

someone who responde to user actions by emitting an event !.

Example Usage

example Login Component

login.js

  class LoginContainer extends React.PureComponent{

    componentDidMount(){
      this.listeners = [
        this.props.listen("LOGIN_START", ()=>this.setState({loading:true})),
        this.props.listen("LOGIN_END", ()=>this.setState({loading:false})),
        this.props.listen("LOGIN_FAILED", (eventName, error)=>this.setState({error})),
      ];
    }

    componentWillUnMount(){
      this.listeners.map(un=>un());
    }

    attemptLogin = (username, password) => {
      this.props.emit("ATTEMPT_LOGIN", {username, password})
    }

    render(){
      return <div>

          {this.state.loading && <Spinner> Please wait ... </Spinner>}

          {this.state.error && <Error> {this.state.error.reason} </Error>}

          <LoginForm
            onSubmit={this.attemptLogin}
            disabled={this.state.loading}
          />

      </div>
    }

  }

  LoginContainer.stateToProps = (store, selectors) => ({
    currentUser: selectors.auth.getCurrentUser(store),
  })

  export default withCore(LoginContainer);

above you will notice few things.

  • using listen and emit, makes the UI very clean !
  • this.props.listen return a function that unsubscribe listerner, so we call this function on unmount to clear all subscriptions.
  • we will not do any validation in UI, its the SDK job to do it and emit LOGIN_FAILED if it fail, this allow for maximum code sharing between project.
  • RULE OF THUMB: if its not Enviroment dependent code, try to move it to SDK package.

example SDK code

  // selectors.js
  export const getCurrentUser =  store => store[types.mountKey] || reducer.initialState;

  // reducer.js
  export function reducer(state=reducer.initalState;, action){
    if(action.type === 'LOGIN_SUCCESS'){
      return action.data;
    }
    return state
  }
  reducer.initalState = {};
  reducer.eventName = ['LOGIN_SUCCESS']; // only get called if action.type === 'LOGINSUCCESS'


  // action.js
  async function loginActionCreator(event, data, emit){

    // if(event !== 'LOGIN_ATTEMPT')return; // we dont need this, because we used eventName bellow.

    // step 1: validate Data;
    // -----------------------

    if( !data.username )
      return emit('LOGIN_FAILED',{message:'username is required'});
    if( !data.password )
      return emit('LOGIN_FAILED',{message:'password is required'});

    // step 2: trying to login
    // -----------------------

    emit('LOGIN_START');
    let user;
    try{
      user = await api.post('/login', data);
    }catch(e){
      emit('LOGIN_FAILED',{message:'username or password doesnot match'});
    }
    emit('LOGIN_END');

    if(user && user.data.token){
      // login success, and data has my token

      //step 3: return action to reducer to change appState
      // --------------------------------------------------
      return {
        type: 'LOGIN_SUCCESS',
        data: user.data,
      }
    }
  }
  loginActionCreator.eventName = 'LOGIN_ATTEMPT'; // only responde to 'LOGIN_ATTEMPT'

Thats It, now you have your smart actionCreator inside your ./sdk folder contain all business logic, and you have kept your ui logic clean and as minimal as possible.

this allow you to share ./sdk folder with your mobile/web/other project easily, you can pack it into its own npm package and simply npm install it. MAXIMUM code share :)

So how it works ?

we give Action creators and connected components some super powers. so Typical life cycle is as so.

  • On UI part User Create An Event(click on button, or scroll, or whatever)
  • Component Respond to that action by calling this.props.emit("EVENT_NAME",eventData);
  • On SDK side Actions listening for this "EVENT_NAME" will get triggered -asyncly-
  • Action can Do 2 things now -since this can be async function-
    • return plain action -an object with type: prop-
    • emit another event (which will reset this cycle)
  • Reducer receive event returned by ActionCreator and change state !
  • All connected components get notified !

Why we think this is better ?

  • UI designers now need to worry about just Emitting Events, no more complex bindActionCreator, or magical functions gets imported and injected into our component
  • App developers can develope whole SDK in conjunction with backend, without worrying about front end or presentation.
  • Its much faster -and safer- to run only reducers who subscribe to an event, not all reducers in chain !

Example

index.js

import { actions, rootReducer, selectors } from "./sdk";

// or whatever logic you want to persist !
const loadedFromDisk = JSON.parse(localStorage.myApp || "{}");
const saveToDisk = state =>
  localStorage.setItem("myApp", JSON.stringify(state));

React.Render(
  <Provider
    reducer={rootReducer}
    actions={actions}
    selectors={selectors}
    onChange={saveToDisk}
    initalState={loadedFromDisk}
  >
    <MyApp />
  </Provider>,
  rootEl
);

your sdk/index.js would look something like

import Users from "./auth";
import Todos from "./todos";
import { combineReducers } from "react-simple-flux";

export const rootReducer = combineReducers({
  [User.types.mountKey]: Users.reducer,
  [Todos.types.mountKey]: Todos.reducer
});

export const selectors = [...Users.selectors, ...Todos.selectors];

export const actions = [...Users.actions, ...Todos.actions];

every time you create a new module inside sdk folder, just add a referenec for it in you sdk/index.js file so that it would be included in your app.

A module folder recommended folder structure would be

  • index.js
  • actions.js
  • selectors.js
  • types.js
  • reducer.js

Recommendation: create a small node/terminal tool that help you generate such boilerplate, for a better development experience !, we included a generator.js inside our ./src folder as an example.

index.js

import reducer from "./reducer";
import actions from "./actions";
import * as selectors from "./selectors";
import * as types from "./types";

export default {
  reducer,
  actions,
  selectors,
  config
};

reducer.js

import { ONLOAD } from "./types";
const initialState = {};

function userReducer(state = initialState, action, store) {
  return state;
}
userReducer.eventName = [ONLOAD];
userReducer.initialState = initialState;

export default userReducer;

actions.js

import { ONLOAD } from "./types";
// import API from "../../api";

async function loadAction(eventName, data, emit, getState) {
  // - i can await an api call before i return !

  // -OR i can also re-emit an event
  // but careful not fall into a loop !

  // emit("API_STARTING",data);
  // const data = api.get();
  // emit("API_END",data);

  // very useful for showing spinner !

  return null; // since no object with {type:''} this will not trigger any reducer.
  // useful when api fail, no need to trigger reducers..

  // example if api success and you need to call reducers
  // return {
  //   type: ONLOAD,
  //   data: whatever
  // }
}
loadAction.eventName = ONLOAD;

export default [loadAction];

selectors.js

  import * as types from './types';
  import reducer from './reducer';

  export const getTodos = store => store[types.mountKey] || reducer.initialState;

Todo

  • improve HOC function, may be implementing shouldComponentUpdate if its proved to be worth it.
  • consider moving logic to its own worker.
  • consider enabling remote Event sourcing keep state tree on remote host !.
1.4.2

7 years ago

1.4.1

7 years ago

1.3.0

7 years ago

1.2.0

7 years ago

1.1.0

7 years ago

1.0.0

7 years ago