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 componentcreateConsume: old-school higher order componentcreateFormandcreateField: creates utilities to assist with formscreateApi: creates an Api component, that provides only app's api and the app itselfcreateComponents: 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 asconsumecreated bycreateConsume, but utilizes the app provided by theProviderProvider: provides an app to the sub-treeConsumer: same asConsumercreated bycreateConsumer, but utilizes the app provided by theProviderApi: same asApicreated bycreateApi, but utilizes the app provided by theProviderForm: same asFormcreated bycreateForm, but utilizes the app provided by theProviderField: same asFieldcreated bycreateField, but utilizes the app provided by theProvider
Context based versions of react helpers is useful when you need reusable components that utilize different apps.
Example
// 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>
}