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`_: .. _`Redux`: https://redux.js.org/ .. code-block:: console $ 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: .. code-block:: js 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 ````, similar to what we did for the ```` in ``athena.jsx``: .. code-block:: jsx import { Provider } from "react-redux"; ... import Modules from './routes/modules.jsx'; import store from './store.jsx'; ... return (
} /> } />
); 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: .. code-block:: 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) { let modulesList = []; if (props.modules) { modulesList = props.modules.data.map((module) => { let key = `module-${module.id}`; let path = `${module.id}`; return ( {module.attributes.code} {module.attributes.name} ); }); } /** * Render the component */ return (

My Modules

{modulesList}
Code Name Sections Actions
); } 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: .. code-block:: js 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``: .. code-block:: 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.