Viewing and Updating Data ========================= Next, we will look at viewing and updating together, because they share a lot of functionality. Viewing a single Module ----------------------- As with the ``routes/modules/new.jsx``, the first step is to load the viewing component in the ``routes/modules.jsx``: .. code-block:: jsx import React from "react"; import { Route, Routes } from "react-router-dom"; import Index from './modules/index.jsx'; import New from './modules/new.jsx'; import View from './modules/view.jsx'; export default function Module() { return ( } /> } /> } /> ); }; As in Ember, we can use ":xxxx" to define a dynamic route segment, here to hold the id of the module to view. Next, create a new file ``routes/modules/view.jsx`` and add the following code: .. code-block:: jsx import React, { useEffect } from "react"; import { connect } from "react-redux"; import { readEndpoint } from "redux-json-api"; import { Link, useParams } from "react-router-dom"; function View(props) { const params = useParams(); useEffect(() => { props.dispatch(readEndpoint(`modules/${params.mid}`)).then((state) => { state.dispatch( readEndpoint(`users/${state.body.data.relationships.teacher.data.id}`) ); }); }, []); /** * Render the component */ if (props.modules && props.modules[params.mid]) { let module = props.modules[params.mid]; let user = { attributes: {}, }; if (props.users && props.users[module.relationships.teacher.data.id]) { user = props.users[module.relationships.teacher.data.id]; } let editPath = `/${module.id}/edit`; return (

{module.attributes.code} {module.attributes.name}

Semester
{module.attributes.semester}
Contact
{user.attributes.email}
Edit
); } else { return
Loading...
; } } function mapStateToProps(state) { let props = { modules: null, users: null, }; if (state.api && state.api.modules) { props.modules = {}; state.api.modules.data.forEach((module) => { props.modules[module.id] = module; }); } if (state.api && state.api.users) { props.users = {}; state.api.users.data.forEach((user) => { props.users[user.id] = user; }); } return props; }; export default connect(mapStateToProps)(View); There are a few interesting aspects we need to look at there. The first is when we load the data: .. code-block:: js const params = useParams(); useEffect(() => { props.dispatch(readEndpoint(`modules/${params.mid}`)).then((state) => { state.dispatch( readEndpoint(`users/${state.body.data.relationships.teacher.data.id}`) ); }); }, []); First, we can access the dynamic component in our URL using the ``useParams`` hook, that we can use in our ``readEndpoint`` call (``mid`` at the end, because in the route we called the dynamic segment ":mid"). By instructing the ``readEndpoint`` action to fetch "modules/" + the id of the module, we get a single module back. As we already know from creating new data, the ``dispatch`` returns a promise with the updated state. The updated state has an additional property ``body`` which contains the response body. We can use that to access the id of the creator of the module, which we then pass to the ``readEndpoint`` action again, but this time loading the "user" with that identifier. We need to do this, because unlike in Ember, the Redux JSONAPI does not automatically deal with relationships. The other big change is in the ``mapStateToProps`` function: .. code-block:: js function mapStateToProps(state) { let props = { modules: null, users: null, }; if (state.api && state.api.modules) { props.modules = {}; state.api.modules.data.forEach((module) => { props.modules[module.id] = module; }); } if (state.api && state.api.users) { props.users = {}; state.api.users.data.forEach((user) => { props.users[user.id] = user; }); } return props; }; The ``state.api.modules`` and ``state.api.users`` always contains a list of all modules or users that have been loaded. However, we only want to access one specific module and user. What we does do is when we translate the state into the props for the component, we transform the two lists into two dictionaries, where in both dictionaries the id of the object is the key. This has the advantage that in the component we can then access the specific module we want to see via its id (which we have in ``params.mid``). In any case, do not forget to return the ``props`` or your app will break. The various ``if`` statements are there, because before the AJAX requests return, the state will not have a ``api`` or ``api.modules`` / ``api.users`` keys, as those have not yet been loaded. Finally, our code uses the props in the ``return`` statement to generate the output: .. code-block:: js let module = props.modules[params.mid]; let user = { attributes: {} }; if (props.users && props.users[module.relationships.teacher.data.id]) { user = props.users[module.relationships.teacher.data.id]; } In the first line, you can see how we use the id (``params.mid``) to access the one specific module with the id from the dynamic route segment. Then, we first create a dummy user and then check whether the user has already been loaded from the API (if there is an entry in ``props.api.users`` with the creator id) and if so, update our dummy user. This is necessary, because after the module is loaded from the API, the component is re-drawn, while loading the user happens in parallel. Thus we need to handle the case where the module has been loaded, but the user not yet. You can try navigating the different modules, to see that it works. You will also see, that because of the nested requests, sometimes there is a minimal visual flicker as data is loaded and re-rendered. Updating a Module ----------------- Updating a module is basically a mix of the new and viewing functionality. First, add the necessary route to ``routes/modules.jsx``: .. code-block:: jsx import React from "react"; import { Route, Routes } from "react-router-dom"; import Index from "./modules/index.jsx"; import New from "./modules/new"; import View from "./modules/view"; import Edit from "./modules/edit"; export default function Modules(props) { return ( } /> } /> } /> } /> ); } Then, create the file ``routes/modules/edit.jsx`` and add the following code: .. code-block:: jsx import React, { useState, useEffect } from "react"; import { connect } from "react-redux"; import { readEndpoint, updateResource } from "redux-json-api"; import { Link, Navigate, useParams } from "react-router-dom"; function Edit(props) { // initial state const [code, setCode] = useState(null); const [name, setName] = useState(null); const [semester, setSemester] = useState(null); const [updated, setUpdated] = useState(false); const params = useParams(); // initial render useEffect(() => { props .dispatch(readEndpoint(`modules/${params.mid}`)) .then(function (state) { state.dispatch( readEndpoint(`users/${state.body.data.relationships.teacher.data.id}`) ); }); }, []); function updateModule(ev) { ev.preventDefault(); let module = props.modules[params.mid]; let updatedCode = code || module.attributes.code; let updatedName = name || module.attributes.name; let updatedSemester = semester || module.attributes.semester; props .dispatch( updateResource({ type: "modules", id: module.id, attributes: { code: updatedCode, name: updatedName, semester: updatedSemester, }, relationships: { teacher: { data: { type: "users", id: 1, }, }, }, }) ) .then((data) => { setUpdated(true); }); } /** * Render the component */ if (updated) { let path = `/modules/${params.mid}`; return ; } else { if (props.modules && props.modules[params.mid]) { let module = props.modules[params.mid]; let updatedCode = code || module.attributes.code; let updatedName = name || module.attributes.name; let updatedSemester = semester || module.attributes.semester; let abortPath = `/modules/${module.id}`; return (

Edit {module.attributes.name}

); } else { return
Loading...
; } } } function mapStateToProps(state) { let props = { modules: null, users: null } if (state.api && state.api.modules) { props.modules = {} state.api.modules.data.forEach((module) => { props.modules[module.id] = module; }); } if(state.api && state.api.users) { props.users = {} state.api.users.data.forEach((user) => { props.users[user.id] = user; }) } return props; } export default connect(mapStateToProps)(Edit); You are already familiar with pretty much all of the code that is here. The ``mapStateToProps`` and ``useEffect`` functions are the same as in the ``routes/modules/view.jsx``, the event handling and form is almost the same as in the ``routes/modules/new.jsx``. The big difference are the following lines: .. code-block:: jsx let module = props.modules[params.mid]; let updatedCode = code || module.attributes.code; let updatedName = name || module.attributes.name; let updatedSemester = semester || module.attributes.semester; The problem we face, is that the module data in the ``props``, which we temporarily store in the ``module`` variable, and which comes form the state, is immutable (because state can only change via actions). However, when the user types something, we need to somewhere store this change, before we update the backend API. The easiest way to do this, is to create a component-level state that holds all the attributes we need, but with default values of ``null``. Then, when we get to the bottom three lines of code, we make use of a neat JavaScript feature to keep our code concise. The code ``code || module.attributes.code`` actually reads as "if ``code`` is not false-y then use that value, otherwise use the value from ``module.attributes.code``". The default value of ``null`` that we set is false-y (meaning it is treated as if it were ``false`` in conditional tests). Thus, when the user has not yet provided any input, the stored value (from ``module.attributes.code`` is used). When the user then provides some data, the ``setCode`` event updates the ``code`` to whatever the user typed. In this case the ``code`` is no longer false-y and its value is used and displayed. This way we have an initial value to be shown to the user, but can also handle the user changing the value. When the user submits the form and the ``updateModule`` event is called, we do the same to set the new values either from what the user provided or from the default values loaded from the backend. These are then passed to the ``updateResource`` to actually update the backend API, and we use the same design pattern as in the ``routes/modules/new.jsx`` to set an ``updated`` flag that then causes a ```` to be output.