COMP10013 Dynamic Web Technologies
COURSEWORK
React Application and Formal Documentation
Banner ID: B00311948
Project Name: AnimalSnap PWA DocumentaHon
Academic Year: 2023-2024
Module: COMP10013 – Dynamic Web Technologies
Term: 2
Project Idea Type: My own idea for project Approved by Pablo Salva
ApplicaHon Type: Progressive Web App (PWA) with ReactJS
1. Introduction
1.1. Aim and Overview of AnimalSnap
The "AnimalSnap" PWA, developed in this project, is designed to merge the engaging world of wildlife with the cutting-edge technology of Progressive Web Applications (PWA). At its core, AnimalSnap aims to provide users with a platform. where they can capture, share, and discover photos and details of various animals encountered in their daily lives or during their travels. Leveraging ReactJS for a seamless, app-like user experience, the application allows users to interact with a suite of features including photo capturing through an integrated camera functionality, geolocation tagging for each animal sighting, and a dynamic, interactive map showcasing the global footprint of all animal encounters logged by the user community.
With an intuitive user interface and user experience, the application not only focuses on the ease of capturing and sharing but also emphasises the discovery and exploration aspect, making it an engaging digital companion for animal enthusiasts and casual explorers alike. Whether you're documenting rare species in remote locations or capturing the urban wildlife in your backyard, AnimalSnap serves as a bridge connecting the natural world with the digital realm, fostering a community of conservation and awareness through shared experiences.
1.2. Structure
This documentation provides a walkthrough for the development process of the “AnimalSnap” progressive web application.
Firstly, this documentation will go over the thought process behind the application state and explain why this implementation was chosen over alterative solutions. Furthermore, the documentation will cover how each of the main functionalities of the application were developed and how they tie-in and communicate with the application state.
Additional information will also be provided regarding the implementation of the user interface and the packages and APIs utilised to produce the desired user experience.
2. State Management
2.1. Redux
Redux is a state management library commonly used for medium-to-large React applications. It is the most popular library of its kind and boasts many features that improve the development experience.
The advantage of Redux is that the application state is centralized, meaning the state is more predictable and easier to test and debug. The application's state can also be decomposed into "slices," which reduces code complexity significantly and allows for a separation of concerns. The Redux tooling also considerably improves the development experience enabling the ability to trace exactly when, why, and how the state changed.
I chose redux for this application as it is moderately complex – consisting of routing and multiple slices of application state such as the posts and location.
Alternatively, I could develop the application with a top-level state. However, this would lead to excessive prop drilling as components deeply nested within the tree require the state as props. Consequently, increasing the overall complexity of the application and crippling the reusability of components.
Another alternative solution would be the Context API provided by the React library. However, implementing the context API in such a way would be developing a “mini redux” in terms of functionality while losing out on the additional features provided by Redux.
Finally, this implementation of Redux is aided by the Redux Toolkit, an opinionated toolset created by redux that provides various abstractions and quality of life features - reducing the boilerplate code traditionally required when implementing Redux logic.
2.2. Creating the Store
The configureStore API is used to create the application store. This store is where the application state is contained. It provides a simple configuration and automatically combines reducers and the redux-thunk middleware.
Figure 1 Configuring the Store
2.3. Location
The locationSlice handles the retrieval of the device's location and provides an API that allows for the updating and retrieval of the location state.
Initial State
First, an initial state is declared. This state contains properties for the coordinates of the device. Additionally, it provides meta. information such as the status of the state and any errors that have been produced.
Figure 2 Initial State
Retrieving Location
The getLocation function is a thunk created with the createAsyncThunk API. A thunk is a particular type of Redux function which can contain asynchronous logic; it is essentially middleware that runs each time before the store is updated.
Figure 3 Retrieving Location
The getCurrentPositionAsync is a simple wrapper function I created for the geolocator API. It provides a nice abstraction and cleans up the code inside the thunk.
Figure 4 getCurrentPositionAsync
A thunk dispatches “start/success/failure” actions throughout its life cycle. To illustrate, we can look at the redux development tools and see which actions are dispatched when the application is initialised.
Figure 5 redux development tools
These actions can then be handled with reducers and the “builder callback” notation.
Figure 6 locationSlice
In this scenario, the status is updated to reflect each stage of the process when retrieving the location. The state will be updated with a default latitude and longitude and an error message if unsuccessful. If successful, the reducer will then update the state accordingly with the data returned from the thunk.
Finally, so that components within the application can access this state, selectors have been created, which are helper functions that allow components to read the state. Later chapters will discuss how the state is retrieved and updated by child components.
Figure 7 Helper functions that allow components to read the state.
2.4. Posts
Persisting Data
Before explaining the CRUD implementations of the postSlices, we will first take a detour to discuss how data is persisted on the device.
As this is a progressive web application, I had two options available to me that I could use to persist the data locally. The first option was localStorage, but it had a maximum limit of 5MB and would quickly become a bottleneck. As a result, the only realistic solution was the IndexedDB API, a database implemented within the browser.
After some research into this API, I learned it could be tricky to work with – simple queries can be complex, and extensive boilerplate code was required for basic procedures. As a result, I discovered Dixie, a popular library for the IndexedDB that provides an elegant and simple API.
With the Dexie library, setting up a database can be accomplished in a few code lines.
Figure 8 Importing Dexie
Initial State
The initial state of the postsSlice follows the same pattern as the locationSlice. Of course, in this case, it contains the posts property, which is an empty array that will hold post objects.
Figure 9 Initial State of postsSlice
Creating Posts
Figure 10 addPost function
The addPost is a thunk that accepts a post data as its argument, creates a new post, and persists it in the database. A unique id is generated with the nanoid function, which comes as part of the Redux ToolKit.
The actions dispatched by this thunk are handled once again using the “builder callback” notation.
What is notable is looking at the “fulfilled” case, we see the posts array is updated with the push method. A general rule is state should never be directly mutated. However, in this case the state isn’t being mutated. The Redux toolkit uses the immer library, which allows me to write code in a mutable fashion that is updated immutably behind the scenes—reducing code complexity.
Figure 11 Reducing code complexity
Retrieving Posts
The getPosts thunk is relatively simple. It retrieves the posts from the database and returns the data. The actions dispatched by this thunk are handled once again using the “builder callback” notation.
Figure 12 getPost functionality
Figure 13 Status and Errors
Updating Posts
Updating data is traditionally a more complex procedure. The updatePost thunk accepts a post object as the argument and extracts the id, name, type and description.
The id is first used to check if the post exists. If the post does not exist, an error is returned that will be handled by “builder callback” notation.
As I only want to allow the user to be able to update the name, type, and description properties. I create a new object, spread the properties of the existing post in the object, and then overwrite the name, type, and descriptions properties with the values passed down as the initial argument.
Figure 14 Update Post Functionality
The builder works almost identically to the previous implementation. The only difference in this scenario is if the post is successfully updated. I find the index of the old post that matches the id of the updated position and replace it with the updated post.
Figure 15 Different cases incuding update functionality
Deleting Posts
Deleting post is also very simple, it uses the id and attempts to delete the post with the following id and if successful it will return the id that will be handled by the builder.
Figure 16 Deleting Posts
In this case the builder will filter the state and remove the post which matches the id of the post that was deleted.
Figure 17 Different cases of Deleting Posts functionality
Selectors
Similar to the locationSlice, there are selectors exported that handle the accessing of the state. Taking a further look, you will see that accessing the posts array is written as `state.posts.posts`.
This is due to the name of the object also being named posts, and it contains the posts object. I could update this in the future by renaming the posts array to “items”. However, as it stands, there is nothing wrong with this approach, and this would only be a semantic improvement.
Figure 18 Selectors for state accessing
2.5. Map
The mapSlice was not originally planned as part of the implementation. I expected that the state of the map would be contained inside a parent component as local state. However, I discovered that the map component's parent was unmounted when switching routes, and the state was lost. This resulted in the map component repositioning each time the user navigates pages, leading to an unpleasant user experience.
As a result, the mapSlice was implemented. It has a simple reducer function that updates the state of the map and a selector so the map component can retrieve the state when it remounts, meaning it will “remember” its original position.
Figure 19 mapSlice creatingSlice
3. Setup
The initial setup of the application begins with adding the store to the application with the Redux Provider. I then added the React Router library to handle routing and navigation. The ColorModeScript. component is from the Chakra UI library, and it prevents the flash of white when retrieving the theme data.
Finally, the service worker is registered, which handles the progressive web application features such as caching.
Figure 20 ServiceWorker Registration
On application start up, the useEffect hook will run and dispatch the getLocation and getPost actions from the Redux store.
Figure 21 useEffect with locationStatus dependency
I wanted to create a splash screen that indicated that the content was being loaded to the user (see Appendix A). To achieve I checked the locationStatus and postStatus of the store. While the store is loading, I display the SplashScreen component. Once the loading is completed, the main content is displayed.
Modern devices were extremely quick at loading, which resulted in the splash screen would appear for less than a second, which was a jarring experience. I used the setTimeout function to create an artificial delay to help alleviate this issue.
The Outlet is the dynamic component that is displayed from React Router.
Figure 22 The Outlet Component
The isNestedRoute variable is retrieved from a custom hook I created that uses the React Router API to determine if the browser views a nested route, i.e., “post/new”. This hook allowed me to display components dynamically depending on the route being viewed. Such as showing a back button for each nested route.
Figure 23 isNestedRoute variable is retrieved from a custom hook