redux-fleo
v1.1.8
Published
Redux framework for asynchronous data
Downloads
8
Maintainers
Readme
redux-fleo
redux-fleo is a framework that offers a declarative way to manage asynchronous state in your React redux application. It automates everything related to asynchronous data: tracking requests, updating the store, refreshing you components and more.
- Very easy to use with minimal code
- Can be plugged anywhere in your redux store without disturbing your existing app
- Handles triggering api calls
- Tracks loading and error status
- Handles side effects with optimistic updates
- Automates refetching data
- Uses batch actions to limit multiple renders and improve performance
Installation
npm install redux-fleo
Definitions
Request: An HTTP request call to a source of data (database, backend server…)
Query: A request that reads data from a source.
Mutation: A request that updates the data on a source.
Service: The function that will send the request and return its result. It can use any client (axios, fetch, xhr…)
Getting started
I. Defining your configuration
A configuration is an object that defines your queries and mutations. It can be flat: defining the queries and mutations directly. Or it can be multi leveled, in order to separate your store into multiple slices.
Flat config:
const flatConfig = {
queries: [
{
name: 'user.fetchMany',
defaultValue: [],
service: () => client.get('/users'),
},
{
name: 'users.fetchOne',
service: (id) => client.get(`/users/${id}`),
},
],
mutations: [
{
name: 'user.create',
service: (data) => client.post('/users', data),
},
],
};
Multi level config:
const multiLevelConfig = {
product: {
queries: [],
mutations: [
{
name: 'product.create',
service: (data) => client.post('/products', data),
},
],
},
customer: {
queries: [
{
name: 'customer.fetchMany',
defaultValue: [],
service: () => client.get('/customers'),
},
],
mutations: [],
},
};
II. Plugging the fleo reducer into your store
redux-fleo provides the static function Fleo.createReducer
, which creates a reducer from a configuration. This reducer can be plugged directly into your redux store as a root reducer or as a slice reducer.
Fleo reducer as root reducer:
import { createStore } from 'redux';
import Fleo from 'redux-fleo';
const config = {
queries: [/* your queries */],
mutations: [/* your mutations */]
};
const store = createStore(Fleo.createReducer(config));
Fleo reducer in a redux slice:
import { configureStore } from '@reduxjs/toolkit';
import Fleo from 'redux-fleo';
const config = { queries: [], mutations: [] };
const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
fleo: Fleo.createReducer(config, ['fleo']),
},
});
NB: if redux-fleo’s reducer is not your only reducer, you must provide a path as a second argument to the function Fleo.createReducer
III. Using redux-fleo hooks
redux-fleo offers a varierty of hooks to simplify interacting with your requests and with the store. These hooks handle triggering your service, updating the store with the result and returning the updated store.
We will use the configuration below in the coming examples:
const config = {
queries: [
{
name: 'user.fetchByFilters',
defaultValue: [],
service: (gender, situation) => {
const filters = { gender, situation };
return axios.post('/users', filters).then(({ data }) => data);
},
},
{
name: 'user.fetchOne',
service: (id) => client.get(`/users/${id}`),
},
{
name: 'customer.fetchByCity',
defaultValue: [],
service: (page, city) => {
return client.get(`/customers?page=${page}&city=${city}`);
},
},
],
mutations: [
{
name: 'user.create',
service: (data) => client.post('/users', data),
},
],
};
useQuery: Fetching data with a single request
useQuery is a hook that takes the name of a query and a list of parameters. After the component is mounted, it will call the service declared in your configuration by matching the name of the query. It returns the result of the query along with the status. If the list of parameters change, the hook will call the service again to refetch the data.
Example:
import { useQuery } from 'redux-fleo'
const UserList = () => {
const [users, loading, error] = useQuery('user.fetchByFilters', ['F', 'married']);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error!</div>;
}
return (
<div>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
useMultiQuery: Fetching data with multiple parallel requests
Sometimes we need to send multiple requests at the same time, but unfortunately we can’t use React hooks inside of loops. useMultiQuery takes the name of a query, a matrix of parameters (an array of an array), and sends multiple requests in parallel with each parameters.
Example:
import { useMultiQuery } from 'redux-fleo'
const UsersListByIds = (props) => {
const { ids } = props;
const parametersMatrix = ids.map((id) => [id]);
const [users, loading, error] = useMultiQuery('user.fetchOne', parametersMatrix);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error!</div>;
}
return (
<div>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
usePaginatedQuery: Fetching paginated data
usePaginatedQuery returns many useful variables to implement pagination, the most important ones are:
loadMore
is a function that will call your service and append its the result to the store.data
is an array that have 3 informations. The first is an array that contains the result of every call made by loadMore. The second and the third is the loading and error status of all calls.clear
is a function that will reset the data, it is used when you want to redo the pagination when a dependency changes.
import { usePaginatedQuery } from 'redux-fleo'
const UserPaginatedList = (props) => {
const { city } = props;
const { loadMore, data, clear, loading, error } = usePaginatedQuery('customer.fetchMany');
React.useEffect(() => {
clear();
loadMore({ page: 1, city });
}, [city]);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error!</div>;
}
return (
<div>
<button onClick={() =>
loadMore({ page: users.length / 10 + 1, city })}
>
Load more
</button>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
useMutation: Updating your data
useMutation returns a function that will call your service and dispatch its result in a redux action.
import { useMutation } from 'redux-fleo'
const UserCreate = () => {
const createUser = useMutation('user.create');
return <button onClick={() => createUser({ name: `john` })}>Create user</button>;
};
Updating data after a mutation
Injecting mutation data to the store
Usually when we send out a mutation (create, update, delete…) we want to update the result of the queries that are affected by the change without making another call to the server.
In order to do that, we will use redux-fleo’s configuration object to specify how we want to update our queries results using the attribute subscribe
.
const config = {
queries: [
{
name: 'user.fetchMany',
defaultValue: [],
service: () => client.get('/users'),
subscribe: {
'user.create': (state, action) => [...state, action.data],
},
},
],
mutations: [
{
name: 'user.create',
service: (data) => client.post('/users', data),
},
],
};
subscribe
is javascript object, each key represent a mutation name ( or any redux action ), and each corresponding value is a localized reducers that will only update a portion of the store.
Due to the configuration above, each time a user is created, every query 'user.fetchMany' will have its store updated to add the new user, and every components connected to the store (via redux-fleo hooks) will be updated automatically.
Refetching query data after a mutation
Sometimes, the result of the mutation does not have enough data to update our queries results, in this case we are obligated to refetch the whole query. The configuration offers two attributes:
refresh
will trigger a refetch of the query when a mutation ( or any redux action ) is dispatched, every components will recall the service.refreshIf
like refresh, but takes a function that will return a boolean to determine if the component should resend the request or not
const config = {
queries: [
{
name: 'user.fetchMany',
defaultValue: [],
service: () => client.get('/users'),
refresh: ['rates.cancel','SOME_REDUX_ACTION'],
refreshIf: {
'user.sendNotification': (state, action) => {
return Boolean(state.find(user=> user.id === action.params[0]))
}
}
},
],
mutations: [
{
name: 'rates.cancel',
service: () => client.post('/rates/cancel'),
},
{
name: 'user.sendNotification',
service: (userId) => client.post(`/users/${userId}/notifications`),
},
],
};
Every time the mutation 'rates.cancel' is called or 'SOME_REDUX_ACTION' is dispatched, all components connected to this query via redux-fleo hooks will refetch their data and update themselves. Every time the mutation 'user.sendNotification' is called, only the components that satisfies the condition of the callback will refetch their data.
Hooks API
useQuery
const config = { queries: [
{ name: "myquery", service: (a,c)=> {}, defaultValue: 3 }
]}
const MyComponent = (props) => {
const [
data, // result of the query, as long as request is not over, value = defaultValue
loading, // boolean
error // boolean
] = useQuery('myquery', ['a', {'c':'c'}], {disabled: true, dependencies: []})
/*
'myquery' : name of the query
['a', {'c':'c'}] : params with which the service will be called
=> service('a', {'c': c})
{ : optional config
disabled: true, : as long as disabled is true, the query will not fire
when it becomes false, the query will fire automatically
you can use this if you want to wait for something before call
dependencies : by default, the depdendencies are the params
each time they change, the query will be called again
you can introduce your own depdendencies with this
}
*/
useMultiQuery
const config = { queries: [
{ name: "mymultiquery", service: (a, b)=> {}, defaultValue: 3 }
]}
const MyComponent = (props) => {
const [
data, // array of results of each of the request calls
loading, // boolean
error // boolean
] = useMultiQuery(
'mymultiquery', [['a1', 'b1'], ['a2', 'b2']],
{ disabled: true, dependencies: []})
/*
'mymultiquery' : name of the query
[['a1', 'b1'], ['a2', 'b2']] : array of params of each request
=> service('a1', 'b1') service('a2', 'b2')
{ : optional config
disabled: true, : as long as disabled is true, the query will not fire
when it becomes false, the query will fire automatically
you can use this if you want to wait for something before call
dependencies : by default, the depdendencies are the params
each time they change, the query will be called again
you can introduce your own depdendencies with this
}
*/
usePaginatedQuery
const config = { queries: [
{ name: "myquery", service: (page)=> page, defaultValue: 3 }
]}
const {
loadMore, // function that calls the service and appends its result to data
data, // array of results of every page [1, 2]
loading, // boolean
error, // boolean
clear, // function that clears the results (after clear data is [])
getSentParams // function that returns an array of previous called parameters with loadMore
} = usePaginatedQuery('myquery');
useEffect(()=>{
loadMore(1);
loadMore(2);
}, [])
License
MIT © nachmo5