This is a React web app that implements the specs outlined in an assignment provided by vector.ai I'm gonna walk through my thought process through the various parts and stages below. Hope you have fun reading it :)
This objective of this part is to build a gallery-like React app in which images (with text) can be dragged around and viewed in a modal. In addition to that, the images should have a spinner while they load. I'd say that this took me a total of an hour and a little bit more. Most of which I spent creating the layout. So here's the process along the way
Obviously the first thing that crossed my mind was to evaluate libaries that implemented the features I wanted. So I took a quick google search to find out that there were a few really good packages available for the two tasks at hand: repositioning items and image spinner.
Libraries for the drag and drop feature:
- react-dnd
- beautiful-react-dnd (Built by the amazing folks at Atlassian)
- react-grid-layout (This was my top contender as I've used the vue equivalent before and it's quite pleasant)
But the first two options weren't well suited for a two-row system (which was a fun little gotcha). And by the time I looked at react-grid-layout I'd read about the HTML drag and drop api enough to just build it myself. Plus react-grid-layout requires a cartesian coodinate system to assign positions which seems a bit overkill for this use-case.
So I hooked onto the API events and was able to get the info of the card that was being dragged and the card that was being dropped onto. Then came another significant design choice: How do I mutate the order of the elements?
As of then, the cards were located inside a grid layout and I needed to figure out two things:
- How to display the cards after the mutation
- How to store the change of location in state
The first solution I thought of was to use the array indexes to inform the order to the UI and for mutations I can move the array items as necessary. Then during development, I chose to move just swap values of type and title and keep the actual array element the same. Essentially, creating a fallback using the position field.
Next I needed to build a spinner/loader enabled image component. Surprisingly, I've never built an image loader or spinner animation/element myself. I've always used an 3rd party image component or a organisation-specific image component so this was a fun little adventure. It took only a couple of minutes but I consider it a small fun little milestone to have built my first image loader and drop/drop layout.
The modal for the image viewer was the final part and nothing remarkable to say there except I got stuck listening for the keypress event on a stupid div lol.
All in all was quite fun to do the frontend. Really had to force myself not to fiddle with UI too much and not build excess features.
-
Checkout the branch part1-frontend-freeze
git checkout part1-frontend-freeze
-
Switch to the frontend folder
cd frontend
-
Install dependencies
npm install
-
Serve the app locally using the development build:
npm start
Part 2 was a lot of familiar than it's predecessor. The simplest API that can get the job done would need to do only two things:
- Retrieve the items from a table
- Update the positions of the items in the table
The data model and operations were simple enough to not necessitate the use of an ORM so I just needed a tool that was high level enough to manage the connections and can let me swap between different database engines (SQLite and PostgreSQL for example).
The starlette recommended databases package was just the right answer. Plus it worked with SQLAlchemy & Alembic in case I needed to do more.
Then I made the migrations & seeding part of the on_startup hook of starlette so that was out of the way. Then I simply tested the API for read and update.
The documentation for the recommended starlette docker image was quite simple and I got it up and running in a few minutes. But I did run into a small issues with using pipenv to install a packages. After a few minutes trying to fix it I just assumed that the docker image was using a different way running the uvicorn server so changed the Dockerfile to generate a requirement.txt on the fly and use that to pip install
Then there was the matter of connecting my trusty old PostgreSQL container to the API container. While I could've skipped this step during this stage I know I probably needed this for the deployment stage so I just got it out of the way. Looking back at it, I could've definitely skipped the docker part and completed all of this in 30 minutes tops. But hindsight is 20/20.
-
Checkout the branch part2-making-the-call-freeze
git checkout part2-making-the-call-freeze
-
Switch to the backend folder
cd backend
-
Install dependencies
pipenv install
-
You can connect the app to a database by editing one of the .env templates (postgres, sqlite) and then renaming the file to .env. If you're lazy then sqlite is the way to go. If you skip this step, the app will complain about it.
-
Serve the api locally
pipenv run uvicorn main:app
Route | Method | Accepts body | Returns |
---|---|---|---|
/ | GET | None | Array of all items |
/ | POST | Array of items to be updated in the format {id: , position:} | Status code 200 if accepted, error if not |
In this part, the frontend is to be connected to the API to enable querying the items and conditional updates every 5 seconds. Simple enough. But holding the state reliably on the frontend and comparing it to the saved state was going to be a task. So I went with the approach that was closest fit with the frontend architecture. You can skip straight to the second approach if you want to see what I actually did rather than my process.
This was the obvious approach. Hold the older/saved data into another state variable that we can check against and then save it the differences to the API. It was during this time, I learnt a bit more about React Hooks, for example, I learnt that the state variable is not accessible inside the method that was called during the save process. Then I had to do a bit of research and find out that I had to use useRefs here. They should add that in the React documentation. So I encountered two silly but time consuming bugs that I'd like to highlight:
- Unintended updates to saved state: What I observed here is that any change to the state variable holding the current state was reflected in the state variable holding the saved state. Obviously, I did the usual thing of using Array.from() and then array destructuring before I realised that it's a bit different for an array of objects. So sorted that out.
- Array reordering: My decision to base the rendering of the cards on the basis of an array and then updating position by reordering the array came to haunt me. But I was able to circumvent that by some defensive programming to ensure the array or the order is not corrupted
I was able to get the application working just fine with the history approach and you can find it in the branch history-approach to look at it. It's a bit messy.
Somehow this didn't strike me during the development of the drag and drop feature but I realised that I could easily position the items using the order property in CSS Grid. By mapping the order CSS property to the position property of each array item, I got the positioning perfect. Now updating the layout can be done by simply checking the position property.
After messing around with state variables, I found it better to simply store the old info into LocalStorage and then compare with the current state. I know it be more performant if stored as a state variable but I preferred this approach for now as it would isolate the saved state. And calculating updates was a simple matter of comparing arrays.
-
Checkout the branch part3-tying-it-up-freeze
git checkout part3-tying-it-up-freeze
-
Switch to the backend folder
cd backend
-
Install dependencies
pipenv install
-
You can connect the app to a database by editing one of the .env templates (postgres, sqlite) and then renaming the file to .env. If you're lazy then sqlite is the way to go. If you skip this step, the app will complain about it.
-
Serve the api locally
pipenv run uvicorn main:app
-
Or you can build the docker image and run it from that but I'll explain more in the next part
-
Open a new terminal and switch to the frontend folder
cd ../frontend
-
Install dependencies
npm i
-
Run the React app locally
npm start
This is the deployment stage and I had to get familiar with docker-compose as I'd only worked with vanilla docker before but it turned out to be a handy tool. After a little bit of testing, I was able to get the services running fine. One thing to note is that the starlette APIs weren't serving the react app but an nginx container was. I chose to do this so that I could debug easier and to keep concerns separate.
You don't need to checkout any branches to see the app at this stage, you can simply run it from the main branch. But before you do that, you just need to setup the .env file. You can go ahead and rename the .sample.env file at the root folder to have reasonable defaults:
mv .sample.env .env
Then you can use docker-compose to have the application served at [http://localhost:8888]
docker-compose up
- Duplicate table creation/seeding: The docker image that was recommended in the assignment spins up multiple processes and therefore caused all of the process to try to create and seed the table. While this didn't corrupt the database, it did throw an error on the server processes especially the first time the service was started. I could've sorted this by moving the table creation and seeding to a separate python file as a part of pre_startup script but I circumvented this by setting
restart: unless-stopped
. This sorted the problem but better solution would be to make it part of the pre_startup script and avoid the error.