Giter VIP home page Giter VIP logo

react-recomponent's Introduction

ReComponent

Reason-style reducer components for React using ES6 classes.

npm GitHub license Travis Codecov


A number of solutions to manage state in React applications are based on the concept of a "reducer" to decouple actions from effects. The reducer is a function that transforms the state in response to actions. Examples for such solutions are the Redux library and architectures like Flux.

Most recently this pattern was implemented in ReasonReact as the built-in solution to manage local component state. Similarly to Redux, ReasonReact components implement a reducer and actions to trigger state changes but do so while staying completely inside regular React state. These components are referred as reducer components.

ReComponent borrows these ideas from ReasonReact and brings reducer components to the React ecosystem.

A reducer component is used like a regular, stateful, React component with the difference that setState is not allowed. Instead, state is updated through a reducer which is triggered by sending actions to it.

Installation

npm install react-recomponent --save

Getting Started

To create a reducer component extend ReComponent from react-recomponent instead of React.Component.

With ReComponent state can only be modified by sending actions to the reducer() function. To help with that, you can use createSender(). Take a look at a simple counter example:

import React from "react";
import { ReComponent, Update } from "react-recomponent";

class Counter extends ReComponent {
  constructor() {
    super();
    this.handleClick = this.createSender("CLICK");
    this.state = { count: 0 };
  }

  static reducer(action, state) {
    switch (action.type) {
      case "CLICK":
        return Update({ count: state.count + 1 });
    }
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        You’ve clicked this {this.state.count} times(s)
      </button>
    );
  }
}

Edit ReComponent - Getting Started

The Counter component starts with an initial state of { count: 0 }. Note that this state is in fact a regular React component state. To update it, we use a click action which we identify by its type "CLICK" (this is similar to the way actions are identified in Redux).

The reducer will receive this action and act accordingly. In our case, it will return an Update() effect with the modified state.

ReComponent comes with four different types of effects:

  • NoUpdate() signalize that nothing should happen.
  • Update(state) update the state.
  • SideEffects(fn) run an arbitrary function which has side effects. Side effects may never be run directly inside the reducer. A reducer should always be pure: for the same action applied onto the same state, it should return the same effects. This is to avoid bugs when React will work asynchronously.
  • UpdateWithSideEffects(state, fn) both update the state and then trigger the side effect.

By intelligently using any of the four types above, it is possible to transition between states in one place and without the need to use setState() manually. This drastically simplifies our mental model since changes must always go through the reducer first.

NOTE! You should NEVER call this.send or any sender in componentWillUnmount. If you need to execute a side-effect in componentWillUnmount (e.g. clear a timer) call that side-effect directly.

FAQ

Advantages Over setState

The advantages are similar to those of Redux or really any state management tool:

  1. Decoupling your state transformers from the rest of the code. This can be a little cumbersome when working with React alone since you will scatter a variety of setState inside your components which becomes harder to follow when the component grows. The sender/reducer system simplifies this since you will no longer focus on state changes within the various methods of your component but you’ll think of actions that you want to send which contains all the information as a standalone object. With that, adding additional behavior (like logging) becomes very easy since all you have to do is hook this logic inside the reducer.

  2. Improved maintainability by forcing a structure. With Redux or ReComponent, you have a good overview of all actions that your application can send. This is an amazing property and allows others to easily understand what a component is is (actually) doing. While you can already learn so much by looking at the shape of the state object, you’ll lean even more just by looking at the action types alone. And since it’s not allowed to use setState at all, you can also be certain that all the code inside the reducer is the only place that transforms your state.

  3. Get rid of side effects with Pure State Transformation. By keeping your state changes side effect free, you’re forced into writing code that is easier to test (given an action and a state, it must always return the same new state). Plus you can build extended event sourcing features on top of that since you can easily store all actions that where send to your reducers and replay them later (to go back in time and see exactly how an invalid state occurred).

Why is the reducer static?

To fully leverage all of the advantages outlined above, the reducer function must not have any side effects. Making the reducer static will enforce this behavior since you won’t have access to this inside the function. We identified three situations that could need this inside the reducer:

  1. You’re about to read class properties. In this case, make sure those properties are properly encapsulated in the state object.
  2. You’re about to write class properties. This is a side effect and should be handled using the SideEffects(fn) effect.
  3. You’re accessing a function that is pure by itself. In this case, the function does not need to be a class property but can be a regular module function instead.

Advanced Usage

Now that we‘ve learned how to use reducer components with React, it‘s time to look into more advanced use cases to effectively handle state transitions across bigger portions of your app.

Effects

We‘ve already said that ReComponent comes with four different types of effects. This is necessary to effectively handle side effects by keeping your reducer pure – given the same state and action, it will always return the same effects.

The following example will demonstrate the four different types of effects and show you how to use them:

import React from "react";
import {
  ReComponent,
  NoUpdate,
  Update,
  SideEffects,
  UpdateWithSideEffects
} from "react-recomponent";

class Counter extends ReComponent {
  constructor() {
    super();
    this.handleNoUpdate = this.createSender("NO_UPDATE");
    this.handleUpdate = this.createSender("UPDATE");
    this.handleSideEffects = this.createSender("SIDE_EFFECTS");
    this.handleUpdateWithSideEffects = this.createSender(
      "UPDATE_WITH_SIDE_EFFECTS"
    );
    this.state = { count: 0 };
  }

  static reducer(action, state) {
    switch (action.type) {
      case "NO_UPDATE":
        return NoUpdate();
      case "UPDATE":
        return Update({ count: state.count + 1 });
      case "SIDE_EFFECTS":
        return SideEffects(() => console.log("This is a side effect"));
      case "UPDATE_WITH_SIDE_EFFECTS":
        return UpdateWithSideEffects({ count: state.count + 1 }, () =>
          console.log("This is another side effect")
        );
    }
  }

  render() {
    return (
      <React.Fragment>
        <button onClick={this.handleNoUpdate}>NoUpdate</button>
        <button onClick={this.handleUpdate}>Update</button>
        <button onClick={this.handleSideEffects}>SideEffects</button>
        <button onClick={this.handleUpdateWithSideEffects}>
          UpdateWithSideEffects
        </button>

        <div>The current counter is: {this.state.count}</div>
      </React.Fragment>
    );
  }
}

Edit ReComponent - Effects 1

All side effect callbacks get a reference to the react component passed as the first argument. This is helpful when a side effect needs to send other actions to the reducer. The next example shows how you can leverage this to handle a more complex component that fetches data from a third party and has to handle multiple states:

import React from "react";
import {
  ReComponent,
  NoUpdate,
  Update,
  UpdateWithSideEffects
} from "react-recomponent";

import { fetchData } from "./api";

class Fetcher extends ReComponent {
  constructor() {
    super();
    this.handleRequestStart = this.createSender("REQUEST_START");
    this.handleRequestSuccess = this.createSender("REQUEST_SUCCESS");
    this.handleRequestFail = this.createSender("REQUEST_FAIL");
    this.state = { isFetching: false, result: null };
  }

  static reducer(action, state) {
    switch (action.type) {
      case "REQUEST_START":
        if (state.isFetching) {
          return NoUpdate();
        } else {
          return UpdateWithSideEffects({ isFetching: true }, instance => {
            fetchData().then(
              instance.handleRequestSuccess,
              instance.handleRequestFail
            );
          });
        }
      case "REQUEST_SUCCESS":
        return Update({ result: action.payload, isFetching: false });
      case "REQUEST_FAIL":
        return Update({
          result: "The data could not be fetched. Maybe try again?",
          isFetching: false
        });
    }
  }

  render() {
    return (
      <React.Fragment>
        <button onClick={this.handleRequestStart}>Fetch</button>
        <div>
          {this.state.isFetching && <p>Loading...</p>}
          <p>
            {this.state.result ? this.state.result : 'Click "Fetch" to start'}
          </p>
        </div>
      </React.Fragment>
    );
  }
}

Edit ReComponent - Effects 2

Handling Events

React uses a method called pooling to improve performance when emitting events (check out the guides on SyntheticEvent to learn more). Basically React recycles events once the callback is handled making any reference to them unavailable.

Since the reducer function always runs within the setState() callback provided by React, synthetic events will already be recycled by the time the reducer is invoked. To be able to access event properties, we recommend passing the required values explicitly. The following example will show the coordinates of the last mouse click. To have control over which properties are sent to the reducer, we‘re using send directly in this case:

import React from "react";
import { ReComponent, Update } from "react-recomponent";

class Counter extends ReComponent {
  constructor() {
    super();
    this.handleClick = this.handleClick.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleClick(event) {
    this.send({
      type: "CLICK",
      payload: {
        x: event.clientX,
        y: event.clientY
      }
    });
  }

  static reducer(action, state) {
    switch (action.type) {
      case "CLICK":
        return Update({
          x: action.payload.x,
          y: action.payload.y
        });
    }
  }

  render() {
    const { x, y } = this.state;

    const style = {
      width: "100vw",
      height: "100vh"
    };

    return (
      <div style={style} onClick={this.handleClick}>
        Last click at: {x}, {y}
      </div>
    );
  }
}

Edit ReComponent - Handling Events

Manage State Across the Tree

Often times we want to pass state properties to descendants that are very deep in the application tree. In order to do so, the components in between need to pass those properties to their respective children until we reach the desired component. This pattern is usually called prop drilling and it is usually what you want to do.

Sometimes, however, the layers in-between are expensive to re-render causing your application to become janky. Fortunately, React 16.3.0 introduced a new API called createContext() that we can use to solve this issue by using context to pass those properties directly to the target component and skipping the update of all intermediate layers:

import React from "react";
import { ReComponent, Update } from "react-recomponent";

const { Provider, Consumer } = React.createContext();

class Counter extends React.Component {
  render() {
    return (
      <Consumer>
        {({ state, handleClick }) => (
          <button onClick={handleClick}>
            You’ve clicked this {state.count} times(s)
          </button>
        )}
      </Consumer>
    );
  }
}

class DeepTree extends React.Component {
  render() {
    return <Counter />;
  }
}

class Container extends ReComponent {
  constructor() {
    super();
    this.handleClick = this.createSender("CLICK");
    this.state = { count: 0 };
  }

  static reducer(action, state) {
    switch (action.type) {
      case "CLICK":
        return Update({ count: state.count + 1 });
    }
  }

  render() {
    return (
      <Provider value={{ state: this.state, handleClick: this.handleClick }}>
        <DeepTree />
      </Provider>
    );
  }
}

Edit ReComponent - Manage State Across the Tree

If you‘re having troubles understanding this example, I recommend the fantastic documentation written by the React team about Context.

Flow

Flow is a static type checker for JavaScript. This section is only relevant for you if you‘re using Flow in your application.

ReComponent comes with first class Flow support built in. When extending ReComponent, in addition to the Props and State types required by regular React.Component we need to specify the third generic parameter which should be a union of all actions used by the component. This ensures type-safety everywhere in the code of the component where the actions are used and even allows exhaustiveness testing to verify that every action is indeed handled.

import * as React from "react";
import { ReComponent, Update } from "react-recomponent";

type Props = {};
type State = { count: number, value: string };
type Action = { type: "CLICK" } | { type: "CHANGE", payload: string };

class TypedActions extends ReComponent<Props, State, Action> {
  // NOTE: We use `this.send()` API because it ensures type-safety for
  //       an action's `payload`.
  handleClick = () => this.send({ type: "CLICK" });
  handleChange = (event: Event) =>
    this.send({ type: "CHANGE", payload: event.target.value });

  state = { count: 0, value: "" };

  static reducer(action, state) {
    switch (action.type) {
      case "CLICK":
        return Update({ count: state.count + 1 });
      case "CHANGE":
        return Update({ value: action.payload });
      }
    }
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          You’ve clicked this {this.state.count} times(s)
        </button>
        <input value={this.state.value} onChange={this.handleChange} />
      </div>
    );
  }
}

Check out the type definition tests for an example on exhaustive checking.

Known Limitations With Flow:

  • this.send API for sending actions is preferred over this.createSender. This is because this.createSender effectively types the payload as any (limitation we can't overcome for now), whereas this.send provides full type-safety for actions
  • While it is possible to exhaustively type check the reducer, Flow will still require every branch to return an effect. This is why the above examples returns NoUpdate() even though the branch can never be reached.

TypeScript

In addition to Flow, ReComponent also comes with TypeScript definitions built-in.

You can learn more about our TypeScript support by looking at the declaration and the accompanying tests.

API Reference

Classes

  • ReComponent

    • static reducer(action, state): effect

      Translates an action into an effect. This is the main place to update your component‘s state.

      Note: Reducers should never trigger side effects directly. Instead, return them as effects.

    • send(action): void

      Sends an action to the reducer. The action must have a type property so the reducer can identify it.

    • createSender(actionType): fn

      Shorthand function to create a function that will send an action of the actionType type to the reducer.

      If the sender function is called with an argument (for example a React event), this will be available at the payload prop. This follows the flux-standard-actions naming convention.

  • RePureComponent

Effects

  • NoUpdate()

    Returning this effect will not cause the state to be updated.

  • Update(state)

    Returning this effect will update the state. Internally, this will use setState() with an updater function.

  • SideEffects(this => mixed)

    Enqueues side effects to be run but will not update the component‘s state. The side effect will be called with a reference to the react component (this) as the first argument.

  • UpdateWithSideEffects(state, this => mixed)

    Updates the component‘s state and then calls the side effect function.The side effect will be called with a reference to the react component (this) as the first argument.

Contributing

Every help on this project is greatly appreciated. To get you started, here's a quick guide on how to make good and clean pull-requests:

  1. Create a fork of this repository, so you can work on your own environment.

  2. Install development dependencies locally:

    git clone [email protected]:<your-github-name>/react-recomponent.git
    cd react-recomponent
    yarn install
  3. Make changes using your favorite editor.

  4. Make sure that all tests are passing and that the code is formatted correctly:

    yarn format
    yarn test
    yarn test:types:flow
  5. Commit your changes (here is a wonderful guide on how to make amazing git commits).

  6. After a few seconds, a button to create a pull request should be visible inside the Pull requests section.

License

MIT

react-recomponent's People

Contributors

danielruf avatar giuseppeg avatar nataly87s avatar philipp-spiess avatar vovacodes 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  avatar

react-recomponent's Issues

First class Flow support

This is an RFC, what do you think if we use flow for the whole react-recomponent codebase including tests? This would eliminate the need for maintaining the flow types separately from the actual implementation and make sure the types are always correct.
I believe that having the react-recomponent code type-checked should also simplify the maintenance a bit by adding extra level of safety besides tests.

Using flow in tests will make sure we test against the correct API.

On the downsides, it could be more difficult for some people to contribute if they don't have any experience working with flow before.

Anyways, what is you stance on it?

Enforce purity of reduce method by converting it to static

Hello good people of react-recomponent,

First of all, awesome library. Thanks.

Not only having the reducer method as static would help in keeping that function pure but also it will increase the overall performance by not having to spawn a reduce method for every instance of a certain class.

Not really sure if that would be possible though.

Kind regards,
Alvaro

Add Support for Registering Middleware Functions

Hi. react-recomponent has really small API (which is good), but I was wondering if it is easy to test it. I have some experience with redux-loop, which have a bigger API, but seems a bit easier test (at least it seems to be).

return UpdateWithSideEffects({ isFetching: true }, instance => {
  fetchData().then(
    instance.handleRequestSuccess,
    instance.handleRequestFail
  );
});

to test this, I will need to fo something, like this

const result = reducer(action, state);
expect(result.type).toEqual(UPDATE_WITH_SIDE_EFFECTS);
expect(result.state).toEqual({ isFetching: true });

jest.mock('fetchData'); // resolve success
let instance = { handleRequestSuccess: jest.fn() }
result.sideEffects(instance);
expect(instance.handleRequestSuccess.mock.calls.length).toBe(1);

jest.mock('fetchData'); // resolve failure
let instance = { handleRequestFail: jest.fn() }
expect(instance.handleRequestFail.mock.calls.length).toBe(1);

right? With redux-loop, it looks like this:

return loop(
  {...state,  isFetching: true},
  Cmd.run(fetchData, {
    successActionCreator: handleRequestSuccess,
    failActionCreator: handleRequestFail,
  })
);
const [state, cmd] = reducer(action, state);
expect(state).toEqual({ isFetching: true });
expect(cmd.simulate({success: true, result: 123})).toEqual(handleRequestSuccess(123));
expect(cmd.simulate({success: false, result: 123})).toBe(handleRequestFail(123));

What I'm saying is that side effects handler is not pure - for sure you need to run side effects at some point, but there is one more layer of indirection is missing.

I like this library, thanks for creating it. This ticket is to ask your opinion on this subject.

Hosted link not working

The hosted link [https://github.com/philipp-spiess/react-recomponent] is not working it is redirecting to the same page.
image

Improve Flow Coverage

Current state of Flow coverage doesn't allow to use action.payload in the reducer:

static reducer(action, state) {
    switch (action.type) {
      case "CLICK":
        return Update({ count: action.payload });  // Error here
      case "CLACK":
        return NoUpdate();
      default: {
        absurd(action.type);
        return NoUpdate();
      }
    }
  }

Flow error:

Cannot get action.payload because property payload is missing in object type [1].

     type-definitions/__tests__/ReComponent.js
     152│   static reducer(action, state) {
     153│     switch (action.type) {
     154│       case "CLICK":
     155│         return Update({ count: action.payload });
     156│       case "CLACK":
     157│         return NoUpdate();
     158│       default: {

     type-definitions/ReComponent.js.flow
 [1]  31│     action: { type: ActionType },

I believe that fixing this is very important for everyone using flow in their project

Example Apps

It would be great to have a couple of example apps, similar to Redux and co. inside an examples folder in the repo.

Static reducer TypeScript annotations

Currently when I annotate my static reducer with a State type I get an incompatibility error

error TS2417: Class static side 'typeof MyComponent' incorrectly extends base class static side 'typeof ReComponent'.                                                
  Types of property 'reducer' are incompatible.                                                                                                                                                                     
    Type '(action: Action, state: State) => NoUpdateAction' is not assignable to type '<TState, TAction extends Action, TSideEffect = any>(action: Action, state: TState) => ReducerAction<TState, TSideEffect>'.                           
      Types of parameters 'state' and 'state' are incompatible.                                                                                                                                                         
        Type 'TState' is not assignable to type 'State'.

I've tried to fix this myself by propagating the Component State to the reducer. But TypeScript doesn't allow that: Static members cannot reference class type parameters.

In the end it boils down to properly annotating the static reducer which is easier said then done:
DefinitelyTyped/DefinitelyTyped#16967
microsoft/TypeScript#13462

Make reducer an instance method instead of a static method

  • the class can be abstract
  • it will be more type-safe because you can't pass generic types to static methods in typescript.
  • you won't have to pass this to the side-effect

Alternatively - you can pass the reducer function to the constructor, to make sure that the reducer doesn't have access to the class instance. and this is also more type-safe

TypeScript Definitions

We should consider adding TypeScript definitions before the 1.0.0 release.

However, I have not worked a lot with TypeScript so far. If anyone has experience with it, I would really appreciate your help.

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.