React Expense Manager 1 - Designing (Conceptually) the Frontend
So, the frontend. I will use React for this one. I alternate between React and Vue depending on the day of the week you know? I wouldn’t yet call myself an expert JS developer. After all, I haven’t yet created my own framework. Who knows..maybe one day.. OK so let’s create a react app. Assuming you have node, npm and create-react-app installed:
npx create-react-app expenses_frontend
Yay we’re done. Now let’s think of how we will structure this and what we want to be able to do in the frontend part. Also, what UI library will be use for it? I’ve used Bootstrap in the past, but I feel like something new. Let’s say MaterialUI. Why? Well, my phone runs Android with a nice customised launcher :) . Maybe I’ll create a React Native version of this app after. We’ll make it look nicely on my phone. Do you need more reasons? So let’s install it.
npm install @material-ui/core
Then let’s add the required tags to our index.html file:
...
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
...
Finally, to test that everything works as intended, I changed my App.js to the following:
import Button from '@material-ui/core/Button';
function App() {
return (
<div className="App">
<p>You spent too much already.</p>
<Button variant="contained" color="primary">
Shame!
</Button>
</div>
);
}
export default App;
Now run “yarn start” and check your browser. Do you see a small button? Great! Our app is finished. Now that we have our MVP (what better way to manage expenses than to remind us we’ve already spent too much?), let’s think of its functionality. This has to be done in conjunction with the backend, so feel free to look at that post as well. That being said, let’s pretend that I’m a separate team from myself and that I have my own ideas. Then I will inform myself of those ideas and then I will debate with myself. So what do I want on the frontend?
When I log in, I expect to have some kind of dashboard/overview where I see various indicators I’m interested in. I want to be able to split by categories (as well as having the possibility to add/remove/change categories). I want to be able to browse through my expenses, maybe add some notes, maybe add some receipts. I want to be able to set budgets. I want to be able to search through it. I want to do way too many things so let’s start simple. Let’s start with a view of a list of our expenses so we can sort them. That will be the first step.
Let’s create an Expense component in an expense.js file.
function Expense() {
return (
<>
</>
)
}
export default Expense
What should our Expense have? It should have an amount, a name, a retailer, a category, a date (and maybe time), a currency and a type (Income/Expense). Now that I said the last thing, let’s rename the Expense to Transaction. And since we could have a recurring transaction, let’s also add a boolean recurring property to it. Make sure you rename the expense.js file as well. Ok now in our App.js, let’s create an object that will contain one of these transactions. Oh and let’s also add a transaction ID to it. My example transaction is:
const transaction = {
id: 1,
amount: 25.00,
name: "Rent",
retailer: "Nile Web Services",
category: "Housing and Utilities",
date: "2021-03-24 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
}
Ok let’s pass in this object using props to our Transaction component.
function App() {
return (
<div className="App">
<p>You spent too much already.</p>
<Button variant="contained" color="primary">
Shame!
</Button>
<Transaction transaction={transaction} />
</div>
);
}
Ok so we are passing our transaction down as props. How do we get them in the Transaction component? We can access its props using the props argument, or we can destructure the object. For now, I’m doing the latter.
function Transaction({ transaction: { id, amount, name, retailer, category, date, currency, type, recurring } }) {
return (
<>
<p>Transaction {id}:
<span>{name} for {amount} {currency} @ {retailer}.</span>
<span>{category}</span>
<span>{date}</span>
<span>{type}</span>
</p>
</>
)
}
Please note I am doing a nested object destructuring. Now let’s create a few more transactions and start building a list displaying them. I will have a transactions array with multiple transaction objects in there.
const transactions = [
{
id: 1,
amount: 25.00,
name: "Rent",
retailer: "Nile Web Services",
category: "Housing and Utilities",
date: "2021-03-24 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 2,
amount: 50.00,
name: "Celery",
retailer: "AllTheLetters",
category: "Groceries",
date: "2021-03-23 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 3,
amount: 75.00,
name: "PI services",
retailer: "Nosebook",
category: "Work",
date: "2021-03-22 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 4,
amount: 150.00,
name: "Salary",
retailer: "Banana",
category: "Passive Income",
date: "2021-03-21 13:42:15",
currency: "GBP",
type: "income",
recurring: true
}
]
Now, in our app, let’s iterate through that and add a Transaction component for each one.
function App() {
return (
<div className="App">
<p>You spent too much already.</p>
<Button variant="contained" color="primary">
Shame!
</Button>
{transactions.map(transaction => <Transaction key={transaction.id} transaction={transaction} />)}
</div>
);
}
Should we make it prettier? Yes. But first I’ll rewrite a few of the components using arrow functions. Also, I created a components directory in my src folder and added another component (just a button for now). So what do we have?
src/components/AddTransaction.js src/components/App.js src/components/Transaction.js
I changed my index.js to import my App from the components folder instead.
import App from './components/App';
Then in my App.js:
import Transaction from './transaction'
import AddTransaction from './AddTransaction'
import Container from '@material-ui/core/Container'
const transactions = [
{
id: 1,
amount: 25.00,
name: "Rent",
retailer: "AWS",
category: "Housing and Utilities",
date: "2021-03-24 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 2,
amount: 50.00,
name: "Celery",
retailer: "Alphabet",
category: "Groceries",
date: "2021-03-23 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 3,
amount: 75.00,
name: "PI services",
retailer: "Nosebook",
category: "Work",
date: "2021-03-22 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 4,
amount: 150.00,
name: "Salary",
retailer: "Banana",
category: "Passive Income",
date: "2021-03-21 13:42:15",
currency: "GBP",
type: "income",
recurring: true
}
]
const App = () => {
return (
<div className="App">
<Container maxWidth="md">
<p>You spent too much already.</p>
<AddTransaction />
{transactions.map(transaction => <Transaction key={transaction.id} transaction={transaction} />)}
</Container>
</div>
);
}
export default App;
My AddTransaction.js is just a simple button for now.
import Button from '@material-ui/core/Button'
const AddTransaction = () => {
return (
<>
<Button variant="contained" color="primary" onClick={() => { alert('clicked') }}>Add Transaction</Button>
</>
)
}
export default AddTransaction;
And finally my Transaction.js is:
const Transaction = ({ transaction: { id, amount, name, retailer, category, date, currency, type, recurring } }) => {
return (
<>
<p>Transaction {id}:
<span>{name} for {amount} {currency} @ {retailer}.</span>
<span>{category}</span>
<span>{date}</span>
<span>{type}</span>
</p>
</>
)
}
export default Transaction;
Ok we will do one more thing in this post and that’s for us to be able to filter through these transactions. Basically a Search box (isn’t that the first thing everyone shows how to do in a React post?). You’ll notice that I’m not currently using the Material-UI Data Grid component. Will we use it at some point? Probably. But then how will we learn how to handle events and the onChange handler on an input field??? Ok so I’m gonna hide the AddTransaction button for now and add a TextField.
const App = () => {
const filterTransactions = event => {
console.log(event.target.value)
};
return (
<div className="App">
<Container maxWidth="md">
<p>You spent too much already.</p>
{/* <AddTransaction /> */}
<form noValidate autoComplete="off">
<TextField
id="outlined-basic"
label="Search transactions"
variant="outlined"
onChange={filterTransactions}
/>
</form>
{transactions.map(transaction => <Transaction key={transaction.id} transaction={transaction} />)}
</Container>
</div>
);
}
Notice there’s also a filterTransactions function added. What does it do? It receives a React SyntheticEvent and we console log the event.target.value. What is that? It’s simply the value that is in our TextField. Note that I added an onChange handler function to the TextField. Play around with it and check that it matches what you input in the TextField. But what’s the point of it? Well, now we can use that value to filter our transaction by. For now, we will only search through the transactions name. Later, we will search through every single possible property of a transaction. Let’s add some State to our component though. The first (piece of, is that the right terminology?) state will be a search term.
Let’s import it in our App.js:
import { useState } from 'react'
And then use it in our App component.
const App = () => {
const [searchTerm, setSearchTerm] = useState('')
const filterTransactions = event => {
setSearchTerm(event.target.value)
};
return (
<div className="App">
<Container maxWidth="md">
<form noValidate autoComplete="off">
<TextField
id="outlined-basic"
label="Search transactions"
variant="outlined"
onChange={filterTransactions}
/>
</form>
<p>The search term is currently {searchTerm}</p>
{transactions.map(transaction => <Transaction key={transaction.id} transaction={transaction} />)}
</Container>
</div>
);
}
So what does useState do? It’s a new(ish) React Hook that lets us use state without writing a class. Older versions of React were all about the Class components (you might well prefer it) whereas the later versions tend to use function components. So the useState allows us to hook into the React state feature. The argument we pass to useState is the initial value of the state, in this case an empty string, and it returns a pair of values: the current state and a function that updates it. In the component we access the state using the former (<p>The search term is currently {searchTerm}</p>
) and we set the state using the latter (setSearchTerm(event.target.value)
). If you play around with the input field you will see that the line underneath it updates as you type. Wow, such reactive. Now let’s do something use(state)ful with it (yup, that was terrible).
With the transaction filter implemented, we have:
const App = () => {
const transactions = [
{
id: 1,
amount: 25.00,
name: "Rent",
retailer: "AWS",
category: "Housing and Utilities",
date: "2021-03-24 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 2,
amount: 50.00,
name: "Celery",
retailer: "Alphabet",
category: "Groceries",
date: "2021-03-23 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 3,
amount: 75.00,
name: "PI services",
retailer: "Nosebook",
category: "Work",
date: "2021-03-22 13:42:15",
currency: "GBP",
type: "expense",
recurring: false
},
{
id: 4,
amount: 150.00,
name: "Salary",
retailer: "Banana",
category: "Passive Income",
date: "2021-03-21 13:42:15",
currency: "GBP",
type: "income",
recurring: true
}
]
const [searchTerm, setSearchTerm] = useState('')
const filterTransactions = event => {
setSearchTerm(event.target.value)
};
const filteredTransactions = transactions.filter(transaction => transaction.name.includes(searchTerm))
return (
<div className="App">
<Container maxWidth="md">
<form noValidate autoComplete="off">
<TextField
id="outlined-basic"
label="Search transactions"
variant="outlined"
onChange={filterTransactions}
/>
</form>
<p>The search term is currently {searchTerm}</p>
{filteredTransactions.map(transaction => <Transaction key={transaction.id} transaction={transaction} />)}
</Container>
</div>
);
}
Play around with it. Please note that at the moment it is case-sensitive. We will improve that when we separate the Search functionality into a new coponents. My App component is getting too long. But how does the “search” work? We have a filteredTransaction array which is essentially the transactions array after it gets filtered by checking which of the transaction contain searchTerm (remember that State?) in the transaction name. Then, in the return(), I am no longer mapping the transactions array but this newly created/updated filteredTransactions array.
And with that, I end the post. In the next one we will split some of the functionality into components. We will also work on making it prettier and we will work on the AddTransaction component. Until then!
Also, the git commit for this post is here