You set up the secretary like this:
import {hire} from 'react-secretary';
import secConfig from '../utils/secConfig';
class Foo extends React.Compoennt {
constructor(props) {
super(props);
this.sec = hire(this, secConfig);
}
}
Then you can use any of the methods to build your component. We'll go over them in no particular order. The config for each feature will be described in the relevant section.
A http client is included that allows you to make http requests and have them safely cleaned up if the component unmounts, either by aborting the request or ignoring the response.
The config for this looks like this:
export default {
http: {
base: '/api/',
abortGetOnUnmount: false,
},
};
First we'll build the request.
const req = this.sec
.http()
.abortOnUnmount()
.get('kittens')
.query({fluffy: true})
.send();
Then we can do a few things. The most basic one is to use it as a promise.
this.setState({loading: true, error: null});
req
.then((body) => {
this.setState({data: body, loading: false, error: null});
})
.catch((errBody) => {
this.setState({data: null, loading: false, error: errBody});
});
That approach gives us the most control. For more normal cases, you may
want to bind the request to state directly, optionally specifying the names
of each state key. When path
is null it's spread into top level state. We
can specify a path like kittens.fluffy
which results in this.state.kittens.fluffy.data
.
req.saveToState({data: 'data', loading: 'loading', error: 'error', path: null});
By default, on unmount we ensure the .then
and .catch
never run, and saveToState
does nothing.
For some requests you may choose to abort the request saving some network usage. .abortOnUnmount()
does this.
The http.abortGetOnUnmount
config option makes this the default behavior. It can be disabled on
a given request with .noAbortOnUnmount()
.
TODO, maybe something with displaying a global alert with customized a component for displaying
errors to users. Should be callable from outside of components. Possibly import {log} from 'react-secretary';
.
Support optional automatic logging of all instance methods. Saves the user from adding a bunch of console.log()
s.
Allow disabling all or some logs from the current component in one place, and global config to set log level.
Global and local config to send some logs to a server endpoint.
log.toJSX
pretty prints some data so that it can be rendered directly in the render method. Good for
debugging ajax and similar. Doesn't end up cluttered with other console logs.
React removed this in es6 classes, we we're adding it back.
this.sec.isMounted() // true or false
Commonly you want to perform some action when a prop changes. The main instances of this are performing
an ajax request (either directly or by dispatching a redux action). There are different definition of 'change'.
By default, it compares with ===
. There are no global config options for this command.
this.sec.onPropChange('id', (props) => {
props.fetchStuff(props.id);
}, {
// default config options
// this argument is optional
onMount: true,
equality: (a, b) => a === b,
});
Further, if you return a sec.http
request object, it will automatically abort or ignore the request
if the prop changes again. If you don't want this, just don't return it.
this.sec.onPropChange('id', (props) => {
return this.sec
.http()
.get('kittens/' + props.id)
.send()
.saveToState({path: 'kittens'});
});
A wrapper around react perf tools? TODO
An implementation of react-future features.
class Kittens extends React.Component {
constructor(props) {
super(props);
this.sec = hire(this, config);
this.sec.future.enable();
}
render(props, state) {
}
componentDidMount(props, state) {
return {newStateKey: value};
}
// etc.
}
Adds event listeners to window or specific elements. Listeners are cleaned up on unmount. In the future a version of react's synthetic events may be added.
The config determines if passive events should be used for certain listeners, if available. Passive events drastically improve performance on mobile, and moderately on desktop.
export default {
events: {
passive: true, // default
target: window, // default
},
};
Listeners can be added with a simple descriptor. This api may need to be simplified, or require less code.
this.sec.events.listen({
target: window, // default, can be an array
passive: true, // default
event: 'scroll', // can be an array
listener: (event, eventType) => {
if (eventType === 'scroll') {
// ...
}
},
});
The render namespace has various utilities that affect render.
This allows you to render a loading indicator that's consistent across the app. You
define this in constructor
and it will replace render's output with the loading indicator
when a condition is met.
The config allows you to specify defaults. If you enable transition
but don't override
transitionName
we'll inject a basic css transition <style>
into the page html.
export default {
render: {
loading: {
when: (state, props) => props.loading || state.loading,
render: (state, props) => <div />, // default
transition: false, // use CSSTransitionGroup or not
transitionName: 'react-secretary-loading-transition',
},
}
};
The options argument is the same structure as the config object.
this.sec.render.loading({
when: (state, props) => state.loading,
render: (state, props) => <LoadingIndicator />,
// etc.
});
Often in custom components you want to merge some of your props into the root element. These props
are className
and style
, a callback ref passed from the parent, and event handlers.
export default {
render: {
combineProps: {
className: true,
style: true,
rootRef: true,
allEventHandlers: false, // enable to pass onClick, etc.
allDomProps: false, // enable to pass tabIndex, etc.
// pass arrays of prop names
// these overried the above settings
whitelist: [],
blacklist: [],
},
},
};
Call the function in constructor
. The global config is the same as the component config.
constructor(props) {
super(props);
this.sec = hire(this, config);
// this allows style, className, rootRef, and onClick
// (assuming default config)
this.sec.render.combineProps({
whitelist: ['onClick']
});
}
This wraps render to support two way data binding. It does this in a react friendly way, including allowing data to be transferred by callback props instead of setting local state, and works with custom components.
The global config is merged deeply into the local config. You'll want to define most of the custom component config globally, and the rest locally.
Since this could be a library itself, you can import it directly and avoid the extra bundle size.
import renderBind from 'react-secretary/lib/renderBind';
renderBind(this, config);
this.sec.render.bind({
components: [
// these are the defaults for custom components
{type: Input, callback: 'onChange', value: 'value'},
},
anyChangeHandler: (newValue, oldValue, path) => {},
changeHandlers: {
'state.search': (newValue, oldValue, path) => {},
},
// allows use of callback instead of local state
// this will make a copy of this.props.data, update it,
// and pass it to this.props.onChange
callbacks: [{
// could be a custom key for sec-bind-callback to allow
// multiple callbacks
key: null,
callback: 'props.onChange',
value: 'props.data',
}],
});
In your render you use special props for binding fields.
render() {
return (
<div>
<Input sec-bind="state.search.text" placeholder="Search..." />
<button sec-bind-toggle={{event: 'click', path: 'state.search.fluffy'}}>
toggle fluffy
</button>
<Input
// could be sec-bind-callback="some-key" which matches 'key' in callbacks[i].key
sec-bind-callback
// this.props.data.text
sec-bind="text"
/>
<div>
);
}
Most applications should use redux, but sometimes it's overkill for simple projects. A global data store is provided. It's made up of buckets which contain key/value pairs. Bucket names and keys can be any type but are compared by reference equality. Items can be accessed by any component.
// bind the store to state
this.sec.store({
bucket: 'kittens', // required
copyToState: 'kittens',
});
// update the key
this.state.kittens.set('oliver', {name: 'oliver'});
// handle the bucket changing yourself
this.sec.store({
bucket: 'kittens',
onChange: (kittensMap, prevKittenMap) => {
if (kittensMap.get('oliver') !== prevKittenMap.get('oliver')) {
this.setState({kitten: kittensMap.get('oliver')});
}
},
});
// bind to a specific key
// only triggers when it changes
// unsubscribe is called for you when the component unmounts
const unsubscribe = this.sec.store({
bucket: 'kittens',
keys: [
{key: 'oliver', onChange: (kitten) => this.setState({kitten})}
]
});