6-8 Hours
To continue the Eventonica UI with more advanced React principles
In this tutorial, you will create a form for creating events, and learn about React useReducer
hooks.
Similar to the Users
list in part 1, create a UI that renders a list of events.
- Create an
Events
component. Copy the<section className="event-management">
section from the start code into this new component. - Create a few mock events at the top of this file. These should at least have a name, description, and ID, and at least 2 other fields. You may use the mockEvents, or create your own.
- Create an
events
state, and initialize it with the mock events - Iterate through each event and display its name, description, etc. For now, the UI can be simple.
We want to create a new form to create events. In part one, you made have used a different useState
hook and variable for each field. Now that there are more fields, notice how creating a new state for each field can get tedious, and the component grows larger.
There is another hook called useReducer which is better for more complex state management. Read this article about the reducer concept. There are 3 main parts of a reducer:
Store, or state: the information you want to store and update. Yours will have all of the event fields.
Actions: how state is updated
For a state like [users, setUsers] = React.useState([])
, setUsers
is the only function that can mutate users
. Similarly, the dispatch
function can "send" specific actions, which are the only way to update the state. Actions typically have a type and a payload.
Reducer A function that accepts the current state and action. It returns the new state.
Look at this example. Try to identify the main parts of the useReducer.
Note: this example uses the spread operator. To review this concept, you can check out this article.
Using the spread operator is a common way objects are modified.
For example, say if there is an object const meal = {"appetizer":"bread", "entree": "Noodles", "dessert": "Ice cream"}
, and you want to update the dessert
field.
You can say meal.dessert = "pie"
. Or, you can say meal = {...meal, dessert:"pie"}
-
Fill in the form
<form id="add-event">
by creating inputs for each event field. These do not have to work yet. -
Initialize the reducer that will store and update the form data.
const EventForm = () => {
const [events, setEvents] = React.useState([]);
const [state, dispatch] = React.useReducer(reducer, initialState);
return <div>...</div>;
};
This will throw an error, since reducer
and initialState
are undefined.
- Create an initial state for the form reducer. This will be an object with keys for each field in the form. The values will be updates as the user fills out the form. For example,
const initialState = {
id: '',
name: '',
date: null,
description: '',
category: ''
};
- Create the reducer function. Each action type will mutate the state in its own way. This reducer function returns a new state.
These usually use a switch statement.
This is one approach, where there is a different action type for each field that is edited. For example, when an action is send with type === "editCategory"
, the category
field of state will be updated. The rest of the fields will remain the same.
There are some example console logs here in places that can help you debug.
const reducer = (state, action) => {
console.log(action, 'this is the action');
switch (action.type) {
case 'editName':
console.log('Logged if the editName action is being dispatched');
return { ...state, name: action.payload };
case 'editDescription':
return { ...state, description: action.payload };
case 'editCategory':
return { ...state, category: action.payload };
default:
return state;
}
};
Add actions types for all of your form fields.
- Actions that affect multiple fields
One of the main benefits of
useReducer
is you can easily update multiple fields with one action.
Say we want the following flow: when the user changes the date of the event, the description and category are reset. Without a reducer, everywhere you change the date, you would have add something like
onChange={(e) =>
setDate(e.target.value)
setDescription('')
setCategory('')
}
This can become tedious, and if you have a larger state where many fields depend on each other, this can become prone to bugs.
In a reducer, this logic becomes a lot cleaner. Instead of updating just the date:
case "editDate":
return { ...state, date: action.payload };
We can update many fields inside this case
:
case "editDate":
return { ...state, description: '', category: '', date: action.payload };
Note: you don't actually have to add this functionality - this was mainly an example to see how a reducer can update multiple fields, and does not have to be a part of your final product.
- Dispatching an action
When connected to the reducer, the event name field could look like this:
<input
type="text"
value={state.name}
onChange={(e) =>
dispatch({
type: 'editName',
payload: e.target.value
})
}
/>
When this input is changed, it will dispatch the "editName"
action. The payload of this action (aka the data), will be the input field value. The reducer will "read" this action, and know to update state.name
. Try testing this. If you console log the state, do you see how it changes?
Dispatch events for all fields in your form.
Check: you should have a form with all the fields needed to create a new event. The reducer state
stores all of these values. When the user types in a field, this should dispatch an action to update the state.
- Adding an event
Now the
state
stores all the data the user entered, but the data doesn't go anywhere. When the user presses submit, it should create a new event object from the field values, and add that event to the list of events usingsetEvents
. That event should then appear in the Events list.
For all the features listed in the main Eventonica README, add code to setup event handlers so the actions change data and refresh the HTML for clear user feedback.
Once all the behavior is fully working, move on to these enhancements.
-
Customize the CSS to style your page. Google Fonts are a great place to find free fonts.
-
Add a README.md with screenshots of your project in your GitHub project repo. This README Template includes most key sections for an open-source app.
Using a Date formatting Library (optional)
You may have noticed that manipulating Date objects and formatting them can be time consuming. There are JS libraries that specialize in this. Install the date-fns library to help easily format the event dates. Using external libraries that specialize in one thing can save you time as a developer, because you won't have to implement these yourself.
Try using the format
function to format the event dates. For example, format(new Date(2021, 10, 1), 'MMM dd yyyy')
.
Bonus: explore the documentation - there are many other helpful functions, like isBefore
, startOfWeek
, addDays
, etc. How could you use this library to implement a "search events by date" function?
Try to do at least one of these challenges to improve your site:
-
Add a
resetForm
action in your reducer. This should clear all form values and set the form to its initial state -
Validate the form before submitting. For example, do not allow events to be created with dates that are in the past
-
Look up all available HTML attributes for
input
and see if you can customize the forms, make some fields required, turn the date search field into a date picker usinginput type="date"
, etc -
Add a variable to save which user is currently "logged in", so saving an event for a user doesn't require typing in the user's ID every time.
-
Use localStorage to store your data so it doesn't get deleted every time the page is refreshed. Learn about localStorage from the MDN localStorage docs
-
Change the UI. Draw up a design for how you'd like your app to look and then code your site to match. The changes could involve breaking the UI into multiple pages, adding more CSS, using a styling library such as Bootstrap, and/or adding more JS. Look at websites that have successful UIs for creating, retrieving, updating, deleting, and filtering data for inspiration. Consider designing a desktop and a mobile version and create a responsive UI.