2.0.4 • Published 6 years ago

react-with-state-props v2.0.4

Weekly downloads
5
License
MIT
Repository
github
Last release
6 years ago

Build Status Coverage Status styled with prettier

react-with-state-props

A container render-prop component to initialize, handle and derive state in React.

Installation

npm install react-with-state-props --save

Examples

Create some state:

import Container from "react-with-state-props"

// ...

<Container
  state={{ counter: 0 }}
  render={props => {
    // props ready-to-go based on the state you provided:
    console.log(props);
    // { counter: 0, setCounter: [Function] }
    // props.setCounter is automatically generated
    return <MyApp {...props} />; // whatever JSX/Comp you want
  }}
/>;

You can derive state (and keep your original state as simple as possible):

<Container
  state={{ counter: 0 }} // original state
  deriveState={[
    // derive `isOdd` when `counter` changes
    {
      onStateChange: ["counter"],
      derive: state => ({
        isOdd: Boolean(state.counter % 2)
      })
    }
  ]}
  render={props => {
    // { counter: 0, setCounter: [Function], isOdd: false }
    return <Counter {...props} />; // your JSX
  }}
/>;

// You can derive state from derived state:

<Container
  state={{ counter: 1 }}
  deriveState={[
    {
      onStateChange: ["counter"],
      derive: ({ counter }) => ({
        isOdd: Boolean(counter % 2)
      })
    },
    {
      onStateChange: ["isOdd"], // now react to `isOdd` changes
      derive: ({ isOdd }) => ({
        isEven: !isOdd
      })
    }
  ]}
  render={props => {
    // { counter: 0, setCounter: [Function], isOdd: true, isEven: false }
    return <Counter {...props} />; // your JSX
  }}
/>;

You can also create custom state handlers:

<Container
  state={{ counter: 0 }}
  withHandlers={{
    add1: props => () => {
      // props.setCounter was created in Container
      props.setCounter(props.counter + 1)
    }
  }}
  render={props => {
    console.log(props);
    // { counter: 0, add1: [Function], setCounter: [Function] }
    return <Counter {...props} />; // your JSX
  }}
/>;

// another example with multiple handlers and some syntax shorthand
// where we want to render a counter that we can increment by 1 or 10, or reset:

<Container
  state={{ counter: 0 }}
  withHandlers={{
    incr: ({ counter, setCounter }) => num => setCounter(counter + num),
    incrBy1: ({ incr }) => () => incr(1), // using custom handler just defined
    incrBy10: ({ incr }) => () => incr(10),
    reset: ({ setCounter }) => () => setCounter(0)
  }}
  omitProps={["setCounter", "incr"]} // drop props before the render function
  render={props => {
    console.log(props);
    // { counter: 0, incrBy1: [Function], incrBy10: [Function], reset: [Function] }
    return <Counter {...props} />; // your JSX
  }}
/>;

Putting it all together, here is a basic Todo App, with the ability to create todos and toggle them between done/undone. We keep the todos in an object for easier lookup (by key), and derive an array of todos on changes that we render:

import React from "react";
import Container from "react-with-state-props";

// define state, derived state and state handlers

const state = {
  todos: {},
  newInput: ""
};

const deriveState = [
  {
    onStateChange: "todos", // when `state.todos` change
    derive: ({ todos }) => ({
      // derive `todosByDate` array
      todosByDate: Object.keys(todos)
        .map(key => todos[key])
        .sort((a, b) => b.stamp - a.stamp)
    })
  }
];

const withHandlers = {
  changeInput: ({ setNewInput }) => e => {
    // controlled text input
    setNewInput(e.target.value); // setNewInput is created from `newInput` state
  },
  mergeTodos: ({ setTodos, todos }) => newTodos => {
    // other handlers will use this
    setTodos({ ...todos, ...newTodos }); // setTodos is created from `todos` state
  },
  submit: ({ mergeTodos, setNewInput, newInput }) => () => {
    // submit new todo
    if (!newInput) return;
    const title = newInput.trim();
    mergeTodos(createTodo(title));
    setNewInput(""); // reset input
  },
  toggleTodo: ({ mergeTodos, todos }) => id => {
    // toggle done state
    const todo = todos[id];
    mergeTodos({
      [id]: {
        ...todo,
        done: !todo.done
      }
    });
  }
};

// Components

const Todos = ({ todosByDate, newInput, changeInput, submit, toggleTodo }) => (
  <div>
    <input type="text" value={newInput} onChange={changeInput} />
    <button onClick={submit}>Submit</button>
    {todosByDate.map(({ id, done, title }) => (
      <div key={id} onClick={() => toggleTodo(id)}>
        {title} {done && " - done"}
      </div>
    ))}
  </div>
);

const App = () => (
  <Container
    state={state}
    deriveState={deriveState}
    withHandlers={withHandlers}
    omitProps={["setTodos", "setNewInput", "mergeTodos"]}
    render={Todos}
  />
);

// implementation details

const uuid = require('uuid');

function createTodo(title) {
  const id = uuid().slice(0, 5);
  return {
    [id]: {
      title,
      id,
      done: false,
      stamp: Date.now()
    }
  };
}

export default App;

That's about it. Enjoy!

Usage

const propTypes = {
  render: PropTypes.func.isRequired,
  state: PropTypes.object.isRequired,
  withHandlers: PropTypes.objectOf(PropTypes.func),
  omitProps: PropTypes.arrayOf(PropTypes.string),
  deriveState: PropTypes.arrayOf(
    PropTypes.shape({
      onStateChange: PropTypes.oneOfType([
        PropTypes.arrayOf(PropTypes.string),
        PropTypes.string
      ]).isRequired,
      derive: PropTypes.func.isRequired
    })
  )
};

Development

react-with-state-props is build in Typescript. PR and Issues welcomed!

Inspirations

  • Andrew Clark's recompose library
  • Kent C. Dodds Advanced React Component Patterns Egghead course
  • Never Write Another HOC talk by Michael Jackson