React

Stapp comes with a bunch of helpers that integrate stapp and React seamlessly. There are two types of these helpers:

Render-prop components

Components created with stapp-react helpers (or exported as-is) follow the render-prop pattern.

type RenderProps<S, A = {}, State = S> = {
  children?: (state: S, api: A, app: Stapp<State, A>) => ReactElement<any> | null
  render?: (state: S, api: A, app: Stapp<State, A>) => ReactElement<any> | null
  component?: ReactType<S & {
    api: A
    app: Stapp<State, A>
  }>
}

Installation

npm install stapp-react stapp stapp-formbase react rxjs
# OR using stapp-cli-tools
stapp install stapp-react

Peer dependencies

  • stapp: >= 2.6
  • stapp-formbase: >= 2.6
  • react: >= 16
  • rxjs: >= 6

Binded components

  • createConsumer: creates a Consumer component
  • createConsume: old-school higher order component
  • createForm and createField: creates utilities to assist with forms
  • createApi: creates an Api component, that provides only app's api and the app itself
  • createComponents: creates all of the above.

createComponents()

type createComponents = (app: Stapp) => {
  Consumer: Consumer,
  consume: ConsumerHoc,
  Form: Form,
  Field: Field
}

NB: consume, Api, Form and Field components are created "on-demand" with corresponding getters. This means that you can safely use createComponents without worrying about unused components.

Consumer, Api, Form and Field components follow the render-prop pattern. See usage examples below.

createConsumer()

type createConsumer = (app: Stapp) => Consumer

type Consumer<State, Api, Result = State> = React.Component<{
  map?: (state: State, api: Api) => Result
} & RenderProps<Result, Api, State>>

Consumer takes an application state, transforms it with mapState (identity by default), then takes an application API, transforms it with mapApi (identity by default) and merges them into one object with mergeProps (Object.assign by default). On each state update, Consumer calls provided children or render prop with a resulting object. If component prop is used, the provided component will be rendered with a resulting object as props.

Most basic example as possible:

import { createConsumer } from 'stapp-react'
import todoApp from '../myApps/todoApp.js'
import ListItem from '../components'

const Consumer = createConsumer(todoApp)

const App = () => <Consumer>
  {
    ({ todos }, { handleClick }) => todos.map((item) => <ListItem
      { ...item }
      key={ item.id }
      onClick={ () => handleClick(item.id) }
    />)
  }
</Consumer>

component prop usage example:

import { createConsumer } from 'stapp-react'
import todoApp from '../myApps/todoApp.js'
import ListItem from '../components'

const Consumer = createContext(todoApp)

const List = ({ todos, handleClick }) => {
  return todos.map((item) => <ListItem
    { ...item }
    key={ item.id }
    onClick={ () => handleClick(item.id) }
  />
}
const App = () => <Consumer component={ List } />

createApi

type createApi = (app: Stapp) => Api

type Api<State, Api> = React.Component<{
  children?: (api: Api, app: Stapp<State, Api>) => ReactElement<any> | null
  render?: (api: Api, app: Stapp<State, Api>) => ReactElement<any> | null
  component?: ReactType<{
    api: Api
    app: Stapp<State, Api>
  }>
}>

Api provides application's api and the app itself.

import { createApi } from 'stapp-react'
import todoApp from '../myApps/todoApp.js'
import ListItem from '../components'

const Api = createApi(todoApp)

const ListItem = ({ text, id }) => <Api>
  {
    ({ handleClick }) => <div>
      {text}
      <button onClick={() => handleClick(id)}>delete</button>
    </div>
  }
</Consumer>

createConsume()

type createConsume = (Consumer: Consumer) => ConsumerHoc
type createInject = (Consumer: Consumer) => ConsumerHoc // theese are aliases

type ConsumerHoc<State, Api, Result> = (
    map?: (state: State, api: Api, props: any) => Result
) => (WrappedComponent: React.ComponentType<Result & { api: Api, app: Stapp<State, Api> }>) => React.ComponentClass

createConsume creates a classic, familiar HoC, that works almost exactly as react-redux @connect.

import { createConsume } from 'stapp-react'
import todoApp from '../myApps/todoApp.js'

const inject = createConsume(todoApp)

const ListItem = inject(
    (state, api, props) => ({
      todo: state.todos.find(todo => todo.id === props.id ),
      handleClick: () => api.handleClick(props.id)
    })
)(({ todo, handleClick }) => {
  return <div onClick={ handleClick }>{ todo.text }</div>
})

const App = inject(
    state => ({ ids: state.todos.map(todo => todo.id) })
)(({ ids }) => {
  return ids.map(id => <ListItem id={ id } key={ id } />)
})

createForm() and createField()

type createForm = (Consumer: Consumer) => Form
type createFiled = (Consumer: Consumer) => Field

type Form = React.Component<RenderProps<FormApi, AppApi, AppState>>

type FormApi = {
  handleSubmit: () => void
  handleReset: () => void
  submitting: boolean // form is in submitting process
  valid: boolean // app has no errors
  dirty: boolean // app has dirty values (that differ from initial values)
  ready: boolean // apps ready state is empty
  pristine: boolean // fields were not touched
}

type Field<State extends FormBaseState, Extra> = React.Component<{
  name: string // field name
  extraSelector: (state: State) => Extra
} & RenderProps<FieldApi<Extra>, AppApi, AppState>>

type FieldApi<Extra = void> = {
  input: {
    name: string
    value: string
    onChange: (event: SyntheticEvent<any>) => void
    onBlur: (event: SyntheticEvent<any>) => void
    onFocus: (event: SyntheticEvent<any>) => void
  }
  meta: {
    error: any // field has an error
    touched: boolean // field was focused
    active: boolean // field is in focus
    dirty: boolean // field value differs from initial value
  }
  extra: Extra
}

These methods create form helpers, who handle every common operation with forms. You can find a comprehensive example in the examples/form-async-validation folder. Note that form helpers are intended to be used with stapp-formbase module (see stapp-formbase documentation).

Basic example:

import { createForm, createField } from 'stapp-react'
import formApp from '../myApps/formApp.js'

const Form = createForm(formApp)
const Field = createField(formApp)

const App = () => {
  return <Form>
    {
      ({ handleSubmit, submitting, valid, pristine }) => {
        <Field name="age">
            ({ input, meta }) => <div>
            <label>Age</label>
            <input { ...input } type="number" placeholder="Age" />
            { meta.error && meta.touched && <span>{meta.error}</span> }
          </div>
        </Field>

        <button
          disabled={ !valid && !pristine && !submitting }
          onClick={ handleSubmit }
        >
          Submit
        </button>
      }
    }
  </Form>
}

Context-based components

  • consume: same as consume created by createConsume, but utilizes the app provided by the Provider
  • Provider: provides an app to the sub-tree
  • Consumer: same as Consumer created by createConsumer, but utilizes the app provided by the Provider
  • Api: same as Api created by createApi, but utilizes the app provided by the Provider
  • Form: same as Form created by createForm, but utilizes the app provided by the Provider
  • Field: same as Field created by createField, but utilizes the app provided by the Provider

Context based versions of react helpers is useful when you need reusable components that utilize different apps.

Example

Edit 8yvv75r050

// app.js
import { createApp, createEvent, createReducer } from 'stapp'

const counterModule = () => {
  const inc = createEvent()
  const dec = createEvent()
  return {
    name: 'test',
    state: {
      counter: createReducer(0)
        .on(inc, (s) => s + 1)
        .on(dec, (s) => s - 1)
    },
    api: {
      inc,
      dec
    }
  }
}

const getApp = () => createApp({
  modules: [counterModule()]
})

export const app1 = getApp()
export const app2 = getApp()

// Buttons.js
import React from 'react'
import { Consumer } from 'stapp-react'
export const Buttons = () => <Consumer>
  {(state, api) => <>
    <p>Current: { state.counter }</p>
    <p>
      <button onClick={api.inc}>Increase</button>{' '}
      <button onClick={api.dec}>Decrease</button>
    </p>
  </>}
</Consumer>

// App.js
import React from 'react'
import { Provider } from 'stapp-react'
import { app1, app2 } from './app'
import { Buttons } from './Buttons'
const App = () => {
  return <>
    <Provider app={app1}>
      <Buttons />
    </Provider>
    <Provider app={app2}>
      <Buttons />
    </Provider>
  </>
}

Hooks

Hooks are a new feature in React 16.8. See more about hooks here.

First of all, all hooks are context-based. We are still exploring the benefits of using binded hooks, and they may be released some time later. Context-based means that you have to use the Provider:

import React, { render } from 'react'
import { Provider, useStapp } from 'stapp-react-hooks'
import { counterApp } from './counterApp'

const Counter = () => {
  const [counter, api] = useStapp(state => state.counter)

  return <button onClick={api.counter.increment}>Times you clicked: { counter }</button>
}

const App = () => <Provider app={counterApp}>
  <Counter />
</Provider>

render(App, rootNode)

Installation

npm install stapp-react-hooks stapp react stapp-formbase
# OR using stapp-cli-tools
stapp install stapp-cli-tools

Peer dependencies

  • react: >= 16.8
  • stapp: >= 2.6
  • stapp-formbase: >= 2.6

useStapp()

Returns a tuple of a state, an application API and an application itself. Accepts an optional selector, which will be applied to an application state.

type Selector<T extends Stapp<any, any>, Result> = (state: StappState<T>, api: StappApi<T>, app: T) => Result;

type useStapp = <T extends Stapp<any, any>, Result = StappState<T>>(
  selector?: Selector<T, Result>
) => [Result, StappApi<T>, T];

See usage example above.

useApi()

Returns an application API.

type useApi = <T extends Stapp<any, any>>() => StappApi<T>
import React from 'react'
import { useApi } from 'stapp-react-hooks'

const Increment = () => {
  const api = useApi()

  return <button onClick={api.counter.increment}>Increment</button>
}

useForm() and useField()

Theese hooks work just as Form and Field components. useForm returns a tuple of FormApi, an application API and an application itself. useField accepts a field name as the only argument and returns a tuple of FieldApi, an application API and an application.

import React from 'react'
import { useForm, useField } from 'stapp-react-hooks'

const MyForm = () => {
  const [form] = useForm()
  const [name] = useField('name')
  const [age] = useField('age')

  return <form onSubmit={form.handleSubmit}>
    <input type='text' {...name.input } />
    <input type='text' {...age.input } />
    <button onClick={form.handleReset} disabled={form.submitting}>Reset form</button>
    <input type='submit' disabled={form.submitting} value='Submit'/>
  </form>
}

results matching ""

    No results matching ""