Full Tutorial on the Best Way to Build Single Page Apps in React with Database Backend
Single Page Applications (SPAs) are widely used these days, and front-end developers have their choice of frameworks to use when designing them. React is one of the more popular options available; its component-based architecture allows for easy re-use of UI elements, and its virtual DOM rendering approach gives it an edge in performance when compared to other frameworks like Angular.
But, an SPA will typically need access to a persistence layer of some kind to store data. Many developers opt for SQL solutions, and these will typically require an API of some kind to retrieve and update data in the database. SlashDB takes care of this API for developers, freeing them to focus on other coding challenges.
In this blog post, we’re going to demonstrate at how quick and easy it is to create a simple SPA using React, SlashDB React SDK and an SQL database.
Example SPA app similar to Google Keep, with source code
The SPA is a task list app, similar in function to apps like Google Keep. You can obtain a working copy of the demo app at https://github.com/SlashDB/taskapp-demo, along with setup instructions. You can find the SlashDB React SDK on GitHub at https://github.com/SlashDB/react-slashdb, or install it directly from the NPM repository at https://www.npmjs.com/package/@slashdb/react-slashdb.
We’ll show you how the SlashDB React SDK was used to create this demo app, giving you a roadmap to quickly create your own SPAs that leverage SlashDB.
The task list app allows users to:
- create and delete task lists
- add/remove/mark completed tasks in each list
- see the percentage of tasks completed in each list
- store task items in a SQL database for persistence
The React SDK provides methods that allow:
- setting up a connection to a SlashDB instance
- basic authentication using an API key or username/password
- CRUD operations on the SlashDB-enabled database using some custom React hooks
Let’s examine some of the components in this demo app to learn how we can use these methods. Note that this isn’t a React tutorial; if you need to brush up on your React terminology, you can start here.
Connecting to the SlashDB Server Using the SlashDBProvider Component
In order to make use of a SlashDB server in our SPA, we need to establish a communication channel between the app and the server. The SlashDBProvider
component in the SDK, combined with a .env
file, allows us to set that up.
In our index.js
file, we’ll pass parameters set in the .env
file to the SlashDBProvider
component as props. The SlashDBProvider
uses a React Context object to share configuration parameters between components. If you’re connecting to SlashDB with a username/password, set the appropriate variables in the .env
file; if you’re using an API key, set the appropriate variable as well as the username variable. The username must correspond to a valid user in the SlashDB configuration. Below is the code used in the demo app:
import { SlashDBProvider } from '@slashdb/react-slashdb'; ReactDOM.render( <HashRouter basename="/"> <SlashDBProvider baseUrl={process.env.REACT_APP_SLASHDB_SERVER_URL} setUpOptions={{ username: process.env.REACT_APP_DATABASE_USERNAME, //password: process.env.REACT_APP_DATABASE_PASSWORD, apiKey: process.env.REACT_APP_USER_API_KEY, }} > <App /> </SlashDBProvider> </HashRouter>, document.getElementById('root') );
Note that our <App>
is wrapped by the SlashDBProvider
so that child components have access to the context object.
App Skeleton and SlashDB Authentication
Onto the App.js
file. We create the app skeleton– note that we’re using the react-dom-router module. We also need to handle authentication to the SlashDB server. We also have our first SlashDB React SDK hook here, useSetUp
. We call this hook to get access to the SlashDBProvider
configuration that we set up before; this hook always needs to be used at least once before using any other SDK hooks. We’ll see it again shortly.
The ProtectedRoute
component contains some logic to make sure that the user has been authenticated, using the SDK-provided Auth
class. Assuming authentication is successful, it renders the ListApp
component.
import { useSetUp } from '@slashdb/react-slashdb'; function App() { useSetUp(); return ( <div> <Header /> <Switch> <Route exact path="/" component={Login} /> <ProtectedRoute exact path="/app" component={ListApp} /> </Switch> </div> ); }
The Login
component brings in the SlashDB configuration (hostname, username, API key/password) from the SlashDBProvider
component by calling the useSetUp
custom hook. useSetUp
returns a special client object that will contain the config, which we pass onto the login method of the Auth
class – we can login to our SlashDB instance now (n.b. if you’re using an API key, you don’t need to use the Auth
class methods, they are for password-based logins). The authentication function in the Login
component is quite simple:
import { useSetUp, auth } from '@slashdb/react-slashdb'; export default function Login(props) { const [username, setUsername] = useState( process.env.REACT_APP_DATABASE_USERNAME ); const [password, setPassword] = useState( process.env.REACT_APP_DATABASE_PASSWORD ); const sdbClient = useSetUp(); const handleSubmit = (event) => { // username and password are provided in a form component, this way you can override the .env defaults if desired auth.login(username, password, sdbClient, () => { props.history.push('/app'); }); event.preventDefault(); };
With the app skeleton and the authentication steps handled, we can move on to retrieving the task list data from the database.
Adding New Task Lists and the Task Lists Container
The ListApp
component rendered by our ProtectedRoute
in turn renders two child components – NewListArea
and Lists. NewListArea
is the component we use to create a new task list. Lists
is a container for the task lists that exist in the database. These components need a way to perform operations against the database via SlashDB; the SDK provides a couple of custom hooks to do just that.
The useDataDiscovery
hook provides access to the Data Discovery features of SlashDB. It takes three parameters: the database name, the resource name, and optionally a filter to apply to the resource. A filter can look something like this:
FirstName/Joe/LastName/Camel
For more on filters, see the SlashDB documentation. The hook returns an array that holds the requested data from the database, as well as four function references which can be used to perform GET, POST, PUT, and DELETE requests with the provided database. Let’s examine the code in ListApp
:
import { useDataDiscovery, auth } from '@slashdb/react-slashdb'; export default function ListApp(props) { const [lists, getList, postList, putList, deleteList] = useDataDiscovery( process.env.REACT_APP_DATABASE_NAME, 'TaskList' ); ... <NewListArea makeNewList={postList} getLists={getList} /> {lists && ( <Lists lists={lists} getList={getList} putList={putList} deleteList={deleteList} />
Here, the useDataDiscovery
hook is given the name of the database as set in the .env
file, and the TaskList
resource, and no filter is given – in other words, we want to retrieve all records in the TaskList
table, with all columns. The records are stored in the lists
array. The function references that are returned get passed along to the NewListArea
component so that new lists can be created, and onto the Lists
component, which will pass them onto its child components.
Here’s how NewListArea
will use the postList
prop (renamed to makeNewList
in this component) that was passed down:
export default function NewListArea(props) { const { makeNewList } = props; const [listName, setListName] = useState(''); ... <input style={inputStyle} value={listName} placeholder="New List..." onChange={(e) => setListName(e.target.value)} /> <button style={buttonWrapper} onClick={() => { makeNewList( { Name: listName ? listName : 'New List', } ); setListName(''); }} > Add list </button>
We provide a name for the new list in the input field, and when the Add button is clicked, the makeNewList function is triggered, with the name of the new list provided – this will call the SlashDB API behind the scenes to add the new list to the TaskList
table.
Our Lists
component is just a container for task lists; we map over lists
and generate a List
component for each one, providing the function references as props, along with the task list record IDs:
<div style={wrapper}> {lists.map((list) => ( <List key={list.TaskListId} TaskListId={list.TaskListId} list={list} getList={getList} postList={postList} putList={putList} deleteList={deleteList} /> ))} </div>
Task Lists
Let’s move onto the two components in this app that contain user data. First we’ll talk about List
.
We import the useDataDiscovery
hook, the useExecuteQuery
hook, and a couple other items – we’ll look at these in a minute. The List component receives some props that contain each task list ID, as well as the function references to perform operations on each list:
import { useDataDiscovery, useExecuteQuery } from '@slashdb/react-slashdb'; import { DataDiscoveryFilter, SQLPassThruFilter, eq } from '@slashdb/js-slashdb'; const List = (props) => { const { TaskListId, list, getList, postList, putList, deleteList } = props;
We’re going to make use of some functions from the SlashDB JavaScript SDK to create the filter parameters for the hooks. DataDiscoveryFilter
is used to create SlashDB-compatible filters for the useDataDiscovery
hook, and SQLPassThruFilter
creates filters for the useExecuteQuery
hook that we’ll be looking at shortly. These aren’t required to create an app (you can build filters yourself), but they can make your life easier if you’re new to SlashDB. We’ll be using these filter parameters in a couple more function calls later in this code.
We run the useDataDiscovery
hook again, this time to retrieve data about the specific tasks in each list. We set TaskItem
as the resource, and provide the DataDiscoveryFilter
object as a parameter to ensure we only retrieve data for the tasks that are associated with the given task list.
const taskListIDPath = new DataDiscoveryFilter(eq('TaskListId',TaskListId)); const queryParams = new SQLPassThruFilter({'TaskListId':TaskListId}); const [tasks, getTasks, postTask, putTask, deleteTask] = useDataDiscovery( process.env.REACT_APP_DATABASE_NAME, 'TaskItem', taskListIDPath );
We get back our array of task items in the tasks
constant, as well as function references to perform operations on each item. We map over the task items and create Task
components for each of them, passing the function references down as props for Task
to use:
{tasks.map((task) => ( <Task key={task.TaskItemId} task={task} getTasks={getTasks} putTask={putTask} deleteTask={deleteTask} executeMyQuery={executeMyQuery} />
Still in the List
component, we’ll be making use of the deleteList
function and the deleteTask
functions, which are called when removing a list. Here’s the delete button for each list:
<button style={removeButtonStyle} onClick={async () => { if (tasks.length > 0) { await deleteTask(taskListIDPath); } await deleteList(taskListIDPath); }} > Delete </button>
When we delete a list, the deleteTask
function is called first, to delete any task items in the list. Then the deleteList
function is called to delete the list itself. Notice the argument that we provide to the deleteList
function is the taskListIDPath
filter that we created previously.
To add a new task item to the list, we call the postTask
function, with some key/value pairs to specify the item name and the task list that the item will belong to:
<button style={addButtonStyle} onClick={async () => { await postTask({ Task: task ? task : 'new task', TaskListId: list.TaskListId, Checked: 0, }).then(() => { executeMyQuery(queryParams); }); setTask(''); }} > Add </button>
Now let’s look at the useExecuteQuery
hook. This hook is used to execute queries that have been previously created in the SlashDB administrative control panel. See the SlashDB documentation for more on SQL Pass-Thru. For the demo app, there is already a query created named percent-complete
on the demo SlashDB instance. It calculates the percentage of task items in a task list that have their Checked column set (i.e. the task items that are marked as complete):
const [queryData, executeMyQuery] = useExecuteQuery( 'percent-complete', queryParams );
To use the useExecuteQuery
hook, we provide the name of the query we want to use, and the SQLPassThruFilter
object that we created previously (we can also provided a JavaScript object of key/value pairs with parameter names and values). The percent-complete
query takes a parameter named TaskListId
. The hook returns an array containing the query result (queryData
) and a function reference to execute the query on demand (executeMyQuery
).
In the List
component, we store the results of the query in the queryData
constant. Then we display the value at the top of the List:
{tasks.length !== 0 && ( <p>Completed tasks {queryData[0].Percentage} %</p> )}
Task Items
Lastly, we’ll examine the Task
component. Remember that in the List
component, each Task
was provided props that held the task item itself, and function references for updating the task item in the database. We also include the function reference for the percent-complete
query, which we’ll call in this component. Let’s use the SlashDB JavaScript SDK to create some filters (again, these are optional but useful). The taskIDPath
constant is the path to the Task
item in SlashDB, which we’ll need when we call any of the get/put/delete functions:
import { DataDiscoveryFilter, SQLPassThruFilter, eq } from '@slashdb/js-slashdb'; const Task = (props) => { const { task, getTasks, putTask, deleteTask, executeMyQuery } = props; const taskIDPath = new DataDiscoveryFilter(eq('TaskItemId',task.TaskItemId)) const queryParams = new SQLPassThruFilter({'TaskListId':task.TaskListId});
Each Task
has a Name
component which can be modified using the putTask
function. The execution of this function is handled inside the Name
component, which we won’t cover here:
<Name style={{ textDecoration: task.Checked ? 'line-through' : 'none', fontSize: '1rem', height: 'auto', margin: 'auto', }} fieldName="Task" fieldValue={task.Task} update={putTask} location={location} ></Name>
We set up the mark complete checkbox like so:
<input style={checkStyle} type="checkbox" checked={task.Checked} onChange={() => { putTask(taskIDPath, { Checked: !task.Checked, }).then(() => { executeMyQuery(queryParams) }); }} ></input>
When the checkbox changes, the putTask
function is called to update the task item’s Checked
column in the database, and then the executeMyQuery
function is called to update the List
component’s completed tasks value. Finally, we have a delete button that triggers the deleteTask
function when clicked, and executes the query again:
<button style={removeButtonStyle} onClick={() => { deleteTask(taskIDPath).then( () => { executeMyQuery(queryParams); }); } } >
Conclusion
We’ve covered all the important functions in the React SDK that you’ll need to get up and running with your own SlashDB-driven SPA – configuration, authentication, data retrieval, and data manipulation. The components and hooks provided in this SDK will streamline your React development process, and SlashDB will take care of generating a fully functional API for your data model.[/vc_column_text][/vc_column][/vc_row]