An incredibly tiny version of flux. Under 50 lines of code.
Goal : pico-flux was made to quickly bootstrap your project with a easy to understand and agnostic implementation of Flux. If you need to make modifications or add features, it's easy to understand what's happening under the hood and make tweaks. As your project grows, it's easy to swap out to another more full-featured flux implementation.
npm install pico-flux
- Three major parts: Stores, Contracts, Smart Components
- Stores: Syncronous event-y data store for smart components. Has builtin of memoization for incredibly fast re-renders.
- Contracts: Asyncronous event-y data store for smart components. Wraps an existing async function and provides methods for tracking in-flight requests, errors, and cached values.
- Smart Components: A wrapper around a React component that responds to events from Stores and Contracts intelligently to re-render. Decoupling your data storage layer from your interfaces.
//TODO: Make one big example here
A Store is an event-emitting data structure to store your app's syncronhous data. You can read more in-depth about them here. Stores have getters
and setters
that are function defined on initialization. getters
extract out values from the store (can also do calculated values as well). setters
update the Store's internal data, while emitting out update events.
Creates a new Store instance. You can optionally pass in your getters and setters on initialization.
const Store = require('pico-flux/store');
let State = { players : [] };
module.exports = Store({
setPlayers : (players)=>State.players = players,
}, {
getPlayers : ()=>State.players
});
Creates new setter
onto the store. The setter
should update values in the Store's state. Will always emit an update
event unless the setter
returns an explicit false
. This is used when the you know that the setter isn't actually changing data in the state. Use this sparringly as it can lead to non-updating code.
Store.setter('addNewPlayer', (name, team)=>{
State.players.push({name, team, score : 0});
})
Store.setter('removePlayer', (name)=>{
const idx = State.players.findIndex((ply)=>ply.name==name);
if(idx == -1) return false;
State.players.splice(idx, 1);
})
Store.addNewPlayer('You', '#BestTeam');
In this example, we don't want an update to fire when we can't find the player to remove, so we return false
.
Creates new getter
onto the store. In pico-flux
, getter
are memoized-once allowing multiple calls with the same parameters back-to-back to be resolved instantly. Whenever a setter
is called, the memoziation for the store will be cleard.
Store.getter('getActivePlayers' ()=>State.players.filter((player)=>!!player.isActive))
Store.getter('getWinningPlayer', ()=>{
return Store.getActivePlayers().reduce((winner, player)=>{
if(!winner) return player;
if(winner.score < player.score) return player;
return winner;
}, false);
});
Store.getWinningPlayer(); //You
In this example, multiple calls to getActivePlayers()
and getWinningPlayer()
will be memozied, allowing us to write straight forward code, while still being performat even if many components are calling these functions.
Manually emits an event. This can be useful in niche situations where there are components that only want to listen to specific type of update. This is quite rare and can usually be solved by just using multiple stores.
Access to the store's Event Emitter. Rarely needed.
A Contract is a long-lived wrapper around an async function. It tracks error and pending states, and will also cache previously called executions. They also ensure that only one request of the async function will be in-flight at a time, even if multiple calls are made. Whenever the state of a contract changes it has a built-in event emitter to emit change notifications. Contracts share the exact same event signature with Stores so they can both be used with Components. Contracts are useful when you have mutilple parts of your application that all rely on a single async source.
Creates a new contract wrapping the passed in async function.
event: 'update'
- Change the name of the eventclientOnly: false
- Will only execute the async function if it's on the client. Automatically set totrue
if the contract is used as a source for a smart component.
const UserContract = Contract(async (userId)=>{
return request.get(`/api/user/${userId}`)
.then((response)=>response.body.user)
});
const PostsContract = Contract(async (postQuery)=>{
return await postDatabase.lookup(postQuery);
});
UserContract('abc123').get();
UserContract.emitter.on('update', ()=>{...});
Access to the contract's Event Emitter
Clears out the contract's internal cache of values, pending/error states, and deferred promises for all instances. Does not emit any events.
Returns an instance of the contract coupled to the provided args
. Calling this with the same args
will return the same instance. Each instance will have it's own set of value cache, pending state, and errors. The provied args
will be passed to the contract's async function whenever it's called.
Executes the async fn
with the args
. This puts several things in motion:
- if there's a request already in-flight, simply returns a new promise coupled with it
- sets the contract instance to pending
- clears the errors
- emits a
execute
andupdate
event on the contract instance. - if successful, caches the returned value, sets pending to false, resolves all promises with the value, and emits
finish
- if not successful, sets the contract's errors, sets pending to false, rejects all promises and emits
oops
- And finally emits another
update
This is useful if you definitely want the async function to run regardless of cached values or error states.
If the value is cached returns it as a resolved promise, otherwise runs contract(...args).execute()
and returns the promise.
A syncronous call to get the value of a contract. If the value is cached, returns it. If there is no cached value and are no errors, it will return undefined
and will also run contract(...args).execute()
behind the scenes.
This is useful for Components, that simply want a value right now, but also to signal to the contract that it should get a value if it doesn't have one cached.
Updates the value of the contract's cache manually, will also emit an update
event. This is useful for initially configuring a contract with known data from another source. A good use case is pre-populating contracts in a isomorphic React app. Send along known data from the server, set
the contracts before render.
Returns true
/false
if the contract currently processing the async function.
Returns the last error from the async function if it was rejected, null
if there are no errors currently. This is cleared before any call to the async funcution.
Returns the value currently cached within the contract instance.
A React Hook that hooks into updates from either a Store or a Contract.
Executes the getter
and stores the value into state in between re-renders. If the source
updates, it will re-execute the getter
and if it's result has changed it will update the state and re-render the component.
//example
If your getter
depends on any passed in props, you must define them in the prop_dependacies
array, so the hook knows to reset the state on certain prop changes.
const Comp = ({ id, content })=>{
const value = useSmartHook(Store, ()=>Store.getValue(id), [id]);
return <div>{value}</div>
}
In this example: the getter
both depends on the value of id
and the Store
. So you must add [id]
to let the hook know it should re-run when the id
changes value
A very useful design pattern is to make a "smart" and a "dumb" version of a component that relies on a store or contract. The "dumb" component is very functional and only uses whatever data is passed in via props, doesn't know that Stores, Contracts, or Actions exist. The "smart" version wraps the dumb component and passes if the relevant information and handlers it needs from various Stores, Contracts, and Actions.
This seperation of concerns allows for unit-testable "dumb" components, and greatly simplifies designing both.
const UserInfoSmart = ({ userId, ...props })=>{
const location = useSmartHook(Store, ()=>Store.getCurrentLocation());
const user = useSmartHook(UserContract, ()=>UserContract(userId, location), [userId, location]);
const onLogout = React.useCallback(()=>Actions.logout(userId), [userId]);
return {
user : user.get(),
pending : user.isPending(),
errors : user.errors(),
onLogout,
...props
};
});
<UserInfoSmart userId={'abc123'} hidden={false} />