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:

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 (
        <Routes>
            <Route index element={<Index />} />
            <Route path="new" element={<New />} />
            <Route path=":mid" element={<View />} />
        </Routes>
    );
};

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:

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 (
            <main className="col-lg-9">
                <div className="row d-sm-flex justify-content-center">
                    <div className="col-6">
                        <h1>
                            {module.attributes.code} {module.attributes.name}
                        </h1>
                        <dl>
                            <dd>Semester</dd>
                            <dt>{module.attributes.semester}</dt>
                            <dd>Contact</dd>
                            <dt>{user.attributes.email}</dt>
                        </dl>
                        <Link to={editPath} role="button">
                            Edit
                        </Link>
                    </div>
                </div>
            </main>
        );
    } else {
        return <div>Loading...</div>;
    }
}

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:

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:

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:

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:

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 (
        <Routes>
            <Route index element={<Index />} />
            <Route path="new" element={<New />} />
            <Route path=":mid" element={<View />} />
            <Route path=":mid/edit" element={<Edit />} />
        </Routes>
    );
}

Then, create the file routes/modules/edit.jsx and add the following code:

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 <Navigate to={path} />;
    } 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 (
            <main className="row justify-content-center">
            <div className="col-lg-6">
                <h1>Edit {module.attributes.name}</h1>
                <form onSubmit={updateModule}>
                <div className="form-group">
                    <label>
                    Code
                    <input
                        type="text"
                        className="form-control"
                        value={updatedCode}
                        onChange={ev => setCode(ev.target.value)}
                    />
                    </label>
                    <label>
                    Name
                    <input
                        type="text"
                        className="form-control"
                        value={updatedName}
                        onChange={(ev) => setName(ev.target.value)}
                    />
                    </label>
                    <label>
                    Semester
                    <select
                        value={updatedSemester}
                        onChange={(ev) => setSemester(ev.target.value)}
                    >
                        <option value="WS18/19">Wintersemester 18/19</option>
                        <option value="SS18">Summersemester 18</option>
                    </select>
                    </label>
                </div>
                <div className="btn-container">
                    <button type="button" className="btn btn-secondary">
                    <Link to={abortPath}>Don't Update</Link>
                    </button>
                    <button type="submit" role="button">
                    Update
                    </button>
                </div>
                </form>
            </div>
            </main>
        );
        } else {
        return <div>Loading...</div>;
        }
    }
}

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:

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 <Redirect> to be output.