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 componentcreateForm
andcreateField
: 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 asconsume
created bycreateConsume
, but utilizes the app provided by theProvider
Provider
: provides an app to the sub-treeConsumer
: same asConsumer
created bycreateConsumer
, but utilizes the app provided by theProvider
Api
: same asApi
created bycreateApi
, but utilizes the app provided by theProvider
Form
: same asForm
created bycreateForm
, but utilizes the app provided by theProvider
Field
: same asField
created 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>
}