Redux#
Note
This documentation is a work in progress. Any help is welcome to fill in the gaps!
As with any other complex React project, the way global state is handled across all components has a big impact on the overall architecture. Basic knowledge of Redux is needed to understand this part, but Volto's use of Redux is "typical" and you can find plenty examples in Volto's code base.
To access the global state, a component needs to be connected with connect
.
A simple example of such component is the
src/theme/ContactForm/ContactForm.jsx
, which is exported connected as:
export default compose(
withRouter,
injectIntl,
connect(
(state, props) => ({
loading: state.emailNotification.loading,
loaded: state.emailNotification.loaded,
error: state.emailNotification.error,
pathname: props.location.pathname,
}),
{ emailNotification },
),
)(ContactForm);
If multiple Higher Order Components need to be used, like in the above example,
the compose
can be used to combine all of them in a final component.
If you're writing Function Components, you can use the useSelector
hook. See
src/components/theme/OutdatedBrowser/OutdatedBrowser.jsx
for an example.
When using the connect
function, you can select
parts from the global store
and either pass them directly as component props, or tweak them combine them,
etc.
You can view the content of the global Redux store by using a browser Redux
developer extension.
The code that is used to populate this store is in the src/reducers
folder.
In some parts of Volto you'll see asyncConnect
being used, which is needed to
enable proper server-side rendering of components. Using it makes sure that the
component will be constructed with the proper data already fetched from the
backend and available as props.
Note
Beware! The asyncConnect
is available only to components that are
attached directly to the router or its children. There are some components
that decide their "rendering path" at render time, so this prohibits the
use of asyncConnect in that component tree. The biggest example of this is
src/theme/View/View.jsx
which decides on the render component based
inspecting the content, so it is not possible to use asyncConnect in any
view/layout component!
Notice the emailNotification
action being passed to connect
in the above
example. All action (which trigger global state updates) need to be passed as
props by connect
. You can't properly trigger an action unless you access it
as a prop, for example this.props.emailNotification()
. For Function
Components you can use the useDispatch
hook.
Global state update fetches are typically triggered by components in the mount
lifecycle stage. See for example src/components/theme/Search/Search.jsx
for
a component that needs to interact with the backend to show its content. In the
Redux flow of information, actions trigger the asynchronous processes and when
that content arrives to the global app, it is pushed as props through the
connect
mechanism. So components only deal indirectly with async information:
they trigger getting that information and it will arrive as a property once it
is ready.
Backend network fetching#
Backend network fetches are automatically triggered by creating a Redux action
with a request
key. For a simple example, see
src/actions/navigation/navigation.js
. In the request
key you can set the
HTTP method type (using the op
field) and the path
to the backend. Any
non-absolute URLs are use the settings.apiPath
prefix, but you can query any
other backend server by using a URL that starts with http://
or https://
.
When writing the reducer counterpart, you'll get the backend response available
as action.result
.
It's also possible to make multiple backend requests at once, for example to
batch create content. In that case, set the action.request
to be a list of
objects (requests) and consequently, in the reducer, the action.result
will
be a list of responses corresponding to each request. See the
src/reducers/content
for an example.
In order to make them more generic and allow more reuse, some actions can
accept a subrequest
parameter, basically a string that can identify the
response and "namespace" it in the global state. See for example the content
reducer. Using subrequest is specially important when using the getContent
action, as, without it, it would overwrite the global state.content
store
with possible wrong content for the current context.
Creating a "request action" potentially triggers some additional access. For
example, even if we only declare the GET_CONTENT
type of action, we can see
that GET_CONTENT_SUCCESS
, GET_CONTENT_PENDING
and GET_CONTENT_FAIL
are
also used in the content
reducer. They are automatically created by the
special API middleware, available in src/middleware/api.js
.
Customizing the Redux middleware#
It is possible to tweak Volto's Redux middleware, for example to add new
middleware by using the config.settings.storeExtender
configuration option.
If you have Redux middleware that you want to insert as the first middleware to
be used, for example, you could configure your project with:
import logAllMiddleware from './example';
export default applyConfig(config) => {
const addLogAll = (middlewares) => {
return [...middlewares, logAllMiddleware];
};
config.settings.storeExtenders = [...config.settings.storeExtenders, addlogAll];
return config;
}