Data via Redux¶
Now that we have our list of modules, we want to fill this with dynamic data, rather than with static data. As with the routing, React does not come with a data library, so we need to integrate a third-party library. The most popular data library for React is called Redux:
$ yarn add redux react-redux redux-thunk redux-json-api
Redux has a fundamentally simple structure, that consists of three components. The “store”, which defines the state-space (which attributes exist and how they are nested) as a standard JavaScript object. As the store may not be modified directly, “actions” are used to define what modifications to the store are allowed. Finally, “reducers” apply the actions to the store to update the content of the store.
To get a basic store, create a new file store.jsx
in the same directory as the athena.jsx
and add the following code:
import React from 'react';
import { createStore } from 'redux';
const initialState = {
api: {
modules: {
data: [
{
type: 'modules',
id: '1',
attributes: {
code: 'INF.06528.01',
name: 'Client-seitige Web-Anwendungen'
}
}
]
}
}
};
function reducer(state, action) {
if (typeof state === 'undefined') {
return initialState
}
return state
}
const store = createStore(reducer);
export default store;
Here we see the three main components. The initialState
variable defines the initial state of our store. Next, the reducer
implements the modifications of the state, depending on the actions. Because the reducer
is also called when the store is initialised, with an undefined
store
parameter, we can use this to return the initialState
, thus initialising our store. For further changes to the state we then would implement the actions here, but for the moment we don’t need any further actions. Finally, we create our store
and return that from the module.
We can now use the store in the other components. To do that, the first step is to wrap the application in a <Provider>
, similar to what we did for the <Router>
in athena.jsx
:
import { Provider } from "react-redux";
...
import Modules from './routes/modules.jsx';
import store from './store.jsx';
...
return (
<Provider store={store}>
<Router>
<div className="container-fluid gx-0">
<header className="row gx-0">
<nav className="navbar navbar-dark bg-dark" aria-label="Main">
<AriaMenu class="navbar-nav horizontal" aria-label="Main">
<li className="navbar-brand">Athena - Study Portal</li>
<li className="nav-item">
<Link
to="/modules"
role="menuitem"
tabIndex="0"
className="nav-link active"
>
My Modules
</Link>
</li>
<li className="nav-item">
<a href="" role="menuitem" tabIndex="-1" className="nav-link">
My Exams
</a>
</li>
</AriaMenu>
<AriaMenu class="navbar-nav horizontal" aria-label="User">
<li className="nav-item">
<Link
to="/login"
className="nav-link"
role="menuitem"
tabIndex="0"
>
Login
</Link>
</li>
<li className="nav-item">
<a
href="login.html"
className="nav-link"
role="menuitem"
tabIndex="-1"
>
Logout
</a>
</li>
</AriaMenu>
</nav>
</header>
<Routes>
<Route path="login" element={<Login />} />
<Route path="modules/*" element={<Modules />} />
</Routes>
<footer>
© 2018 - Mark Hall (
<a href="mailto:mark.hall@informatik.uni-halle.de" tabIndex="0">
mark.hall@informatik.uni-halle.de
</a>
)
</footer>
</div>
</Router>
</Provider>
);
With the store
linked to the application, we can use it in the routes/modules/index.jsx
to load the list of modules from the store, rather than hard-coding it:
import React, { useEffect } from "react";
import { Link } from 'react-router-dom';
import { connect } from "react-redux";
import { readEndpoint } from "redux-json-api";
import AriaMenu from "../../components/aria-menu";
function Index(props) {
let modulesList = [];
if (props.modules) {
modulesList = props.modules.data.map((module) => {
let key = `module-${module.id}`;
let path = `${module.id}`;
return (
<tr key={key}>
<td>
<Link to={path}>{module.attributes.code}</Link>
</td>
<td>
<Link to={path}>{module.attributes.name}</Link>
</td>
<td>
<nav>
<ul role="menu" className="horizontal" aria-label="Sections">
<li>
<a
href=""
role="menuitem"
aria-label="Dates"
title="Dates"
className="mdi mdi-calendar-clock"
tabIndex="0"
></a>
</li>
<li>
<a
href=""
role="menuitem"
aria-label="Documents"
title="Documents"
className="mdi mdi-file-document-box-multiple-outline"
tabIndex="-1"
></a>
</li>
<li>
<a
href=""
role="menuitem"
aria-label="Exercises"
title="Exercises"
className="mdi mdi-test-tube"
tabIndex="-1"
></a>
</li>
<li>
<a
href=""
role="menuitem"
aria-label="Teilnehmerinnen"
title="Teilnehmer_innen"
className="mdi mdi-account-multiple"
tabIndex="-1"
></a>
</li>
</ul>
</nav>
</td>
<td>
<nav aria-label="Actions for Client-seitige Web-Anwendungen">
<ul role="menu" className="horizontal" aria-label="Actions">
<li>
<a
href=""
role="menuitem"
aria-label="Leave"
title="Leave"
className="mdi mdi-delete warning"
tabIndex="0"
></a>
</li>
</ul>
</nav>
</td>
</tr>
);
});
}
/**
* Render the component
*/
return (
<div className="row">
<div className="modules-menu col-lg-3 col-md-2 bg-light">
<h1>My Modules</h1>
<nav className="navbar navbar-light" aria-label="Modules">
<AriaMenu role="menu" aria-label="Modules" orientation="vertical">
<li>
<a role="menuitem">Current Semester</a>
</li>
<li>
<a role="menuitem">Last Semester</a>
</li>
<li role="separator"></li>
<li>
<a href="" tabIndex="-1" role="menuitem">
Enroll in Module
</a>
</li>
<li>
<a role="menuitem" tabIndex="-1">
Create Module
</a>
</li>
</AriaMenu>
</nav>
</div>
<section className="col-lg-9 col-md-10 bg-light">
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Sections</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{modulesList}
</tbody>
</table>
</section>
</div>
);
}
function mapStateToProps(state) {
if (state.api && state.api.modules) {
return {
modules: state.api.modules
}
} else {
return {
modules: {
data: []
}
}
}
}
export default connect(mapStateToProps)(Index);
First of all, the store is available via props
, not via state
. The reason for this is that the store is essentially injected into the component and data that comes from outside into the component always comes via props
.
The list of modules is here available via props.modules
. However, if you look at the data-structure in store.jsx
, you will see that the list of modules is actually inside another key api
(this is in preparation for loading data externally). To translate from the store representation to the props
that we then use in the component, we need to provide a mapStateToProps
function. This takes the current state and returns a new object that is that sub-set (or transformation) of the state that the component itself uses.
In the final step export default connect(mapStateToProps)(Index)
we actually inject the store into the Index
class. What this does is inject the store into the state.props
via the mapStateToProps
function and then load our component with that. To make this work, we removed the export default
from before the function Index
and instead export the result of the connect
function, which is the version of our component with the store injected.
If you now look at the list of modules in the browser, you will see that it only contains the one module defined in the initialState
.
External Data¶
Loading data statically defined in the store is of course not where we want to end up. We want to load the list of modules from the external API. To do this, we need to update the store.jsx
to this:
import React from 'react';
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk';
import { reducer as api, setAxiosConfig } from 'redux-json-api';
const reducer = combineReducers({
api
});
const store = createStore(reducer, applyMiddleware(thunk));
store.dispatch(setAxiosConfig({
baseURL: 'https://mht.uzi.uni-halle.de/client-seitige-web-anwendungen/api/example'
}));
export default store;
As you can see, we now import a lot of additional external components, all of which together implement fetching remote data. All that is needed from us is to configure them together.
The first is that you can see that we have replaced our reducer with a combineReducers
call. We will not go into this in detail, but basically it configures the Redux JSONAPI reducer to be available via the api
key in the store (as we had in our test initialState
).
Next, when we createStore
we need to include the thunk
middleware, which provides the necessary functionality to have asynchronous external AJAX requests.
Finally, we use store.dispatch
to run our first action on our store. This action does not actually change the store that our application directly interacts with, but it is needed to configure the Redux JSONAPI reducer. The reason for this is that the reducer’s configuration is stored in the store, thus to modify it, we need to dispatch an action on the store, which updates the configuration. As in the Ember tutorial, replace the “example” part with your own to access your own instance of the API.
While everything is now configured, we need to actually instruct the store to fetch the external data. To do this, update the top of the routes/modules/index.jsx
:
import React, { useEffect } from "react";
import { Link } from 'react-router-dom';
import { connect } from "react-redux";
import { readEndpoint } from "redux-json-api";
import AriaMenu from "../../components/aria-menu";
function Index(props) {
useEffect(() => {
props.dispatch(readEndpoint("modules"));
}, []);
...
First, we add the readEndpoint
import. This is another Redux action, in this case an action that instructs the Redux JSONAPI reducer to load the external data. We then use this in the useEffect
hook to fetch the external “modules” date. This hook combines three functions of class components, componentDidMount
, componentDidUpdate
and ComponentDidUnmount
. Through this we can use side effects of the component’s life cycle. This hook takes two parameters, the first being a function that handles the action and the second being an array that takes the dependencies which will cause a rerendering of the page. If we leave out the array, useEffect will called whenever something changes, which might cause unnecessary traffic. If we want to only use the effect after the first rendering of the component, we can pass an empty array. If we want certain properties to cause the useEffect-action, we need to pass them to our dependency array. By providing just the type of the class we want to fetch, the external API will return a list of all objects of that type.
If you now look at the module list, you should see the full list of modules from the API.