Implemeting more features Authentication Dashboard etc - vonschappler/Ultimate-React GitHub Wiki
The most common data fetching operarios are juntcions, filtering, sorting and pagination. Below we'll present some simple snippets of code on how to implement those operarions directly on the server side, so the amount of data received by the client is optmized to only what is really requested. For the a fully and more complex code on how to proceed with this operations, check Supabase Documenatation or the final code of the project developed.
Those are queries usualy made in relational database on which we wish to fetch parts of the data instead of pulling all the data from a table.
// query code for selecting specific columns to be retrieved from table1
const { data, error } = await supabase
.from('table1')
.select('column1, column2, ..., column_n');
Those are queries usualy made in relational database on which we wish to fetch data of multiple tables which are co-related each other.
// query code for fetching data from table1 while also pulling the references that table2 provide inside table1
const { data, error } = await supabase
.from('table1')
.select(
'column1, column2, ..., table2_id, table2(column1_table2, column2_table2)'
);
Those are queries usualy made in relational database on which we wish to fetch data of multiple tables which are co-related each other.
// query code for fetching data while filtering the data to match the condition where columnName is equal to value
// there are many other filter operations, all listed on supabase documentation
const { data, error } = await supabase
.from('table1')
.select('*')
.eq('columnName', 'value');
Those are queries usualy made in relational database on which we wish to fetch data of multiple tables which are co-related each other.
// query code for fetching data while sorting the data on the ascending direction
// to change the direaction of sorting, the value of ascending should be set to false
const { data, error } = await supabase
.from('table1')
.select('*')
.order('columnName', { ascending: true });
Those are queries that fetch specific amount of data. for cases where the amount of data fetched is too big.
// query code for fetching limited ammount of data from 1 to 10
const { data, error } = await supabase.from('table1').select('*').range(0, 9);
Pre fetching consist in feching data that we know it may come necessary before it may be required to be rendered on the UI.
In the context of pagination this means that we already should fetch the next (or previous) page before it's effectivelly rendered.
The snippet of code below shows how to implement pre fetching using react query:
// file useBookings.js
import { useQuery, useQueryClient } from '@tanstack/react-query';
// other imports
export function useBookings() {
// implements the client queryClient required for pre fetching
const queryClient = useQueryClient();
// other definitions
// pre fetches the next page
if (page < pageCount)
queryClient.prefetchQuery({
queryKey: ['bookings', filter, sort, page + 1],
queryFn: () => getBookingsApi({ filter, sort, page: page + 1 }),
});
// pre fetches the previous page
if (page > 1)
queryClient.prefetchQuery({
queryKey: ['bookings', filter, sort, page - 1],
queryFn: () => getBookingsApi({ filter, sort, page: page - 1 }),
});
// rest of the code
}
As an alternative, it's also possible to use queryClient.prefetchInfiniteQuery ()
, but for information on how to use this, check the React Query documentation.
Before explaining how to use the Supabase to authenticate an user, it's important to metion that when creating a supabase project, that comes with a defalt User database created for us.
This database can be found inside the Authorization section of your project and it's also by default empty.
Also it's important to mention that Supabase has some built in login methods, which can be enabled under Providers inside the Authorization Section.
With that said, in order to enable authentication while using supabase, first create an user with any email and password you desire.
Then (following the code structure for the project), inside the folder services
, a new file to manage the process of login in an user is defined, similar to the snippet below:
// file apiAuth.js
import supabase from 'path/to/supabase';
export async function login({ email, password }) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw new Error(error.message);
return data;
}
Also, since for the project under development, we decided to use React Query as part of our tech stack, we'll make use of this to manage this state, with a snippet code similar to what's provided below:
// file useLogin.js
import { useMutation } from '@tanstack/react-query';
import { login as loginApi } from 'path/to/apiAuth';
// other imports
export function useLogin() {
const { mutate: login, isLoading: isLoggingIn } = useMutation({
mutationFn: ({ email, password }) => loginApi({ email, password }),
onSuccess: () => {
//this is important so we set manually some data to the query
queryClient.setQueriesData(['user'], user.user);
navigate('/dashboard', { replace: true });
},
onError: (err) => {
console.log(err);
},
});
return { login, isLoggingIn };
}
Based on the structure of the project, the best way to protect access to the application for not logged in users is by encapsulating our ApplLayout
layout component into a protected route.
This happens because in the way that our rotes were defined inside our application are all children of the app layout component. So, the code of AppLayout
should now become something similar to:
// file AppLayout.jsx
import ProtectedRoute from 'path/to/ProtectedRoute';
// other definitions
function AppLayout() {
return (
<ProtectedRoute>
<StyledAppLayout>{/* app layout components*/}</StyledAppLayout>
</ProtectedRoute>
);
}
export default AppLayout;
Because AppLayout
is now encapsulated inside a ProtectedRoute
component, this also needs to be created:
// file ProtectedRoute.jsx
// react imports
import { useUser } from 'path/to/useUser';
import { useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
function ProtectedRoute({ children }) {
const navigate = useNavigate();
const { isLoadingUser, isAuthenticated } = useUser();
useEffect(() => {
if (!isAuthenticated && !isLoadingUser) navigate('/login');
}, [navigate, isAuthenticated, isLoadingUser]);
if (isLoadingUser) return <Spinner />;
if (isAuthenticated) return children;
}
export default ProtectedRoute;
as well as it's also necessary to create the custom hook useUser.js
and add a new function to the apiAuth.js
:
// file apiAuth.js
// other functions definitions
export async function getCurrentUser() {
const { data: session } = await supabase.auth.getSession();
if (!session.session) return null;
const { data, error } = await supabase.auth.getUser();
if (error) {
console.error(error);
throw new Error(error.message);
}
return data?.user;
}
// file useUser.js
import { useQuery } from '@tanstack/react-query';
import { getCurrentUser } from 'path/to/apiAuth';
export function useUser() {
const { data: user, isLoading: isLoadingUser } = useQuery({
queryKey: ['user'],
queryFn: getCurrentUser,
});
return {
user,
isLoadingUser,
isAuthenticated: user?.role === 'authenticated',
};
}
In order to logout an user, all we need is a component to handle the logout process (this component should be rendered on some part of the common components, usually this is rendered on the header of a webiste) and a custom hook which will link up to the component to the logout function created on apiAuth.js
, as shown below:
// file apiAuth.js
// other function definitions
export async function logout() {
const { error } = await supabase.auth.signOut();
if (error) {
console.error(error);
throw new Error(error.message);
}
}
// file useLogout.js
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { logout as logoutApi } from 'path/to/apiAuth';
import { useNavigate } from 'react-router-dom';
export function useLogout() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { mutate: logout, isLoading: isLoggingOut } = useMutation({
mutationFn: logoutApi,
onSuccess: () => {
// this is important so all queries saved in the cache are deleted
queryClient.removeQueries();
navigate('/login', { replace: true });
},
onError: (err) => {
console.error(err);
},
});
return { logout, isLoggingOut };
}
// file Logout.jsx
import { useLogout } from 'path/to/useLogout';
function Logout() {
const { logout, isLoggingOut } = useLogout();
return (
<button disabled={isLoggingOut} onClick={logout}>
Logout
</button>
);
}
export default Logout;
To create a new user, yet some pieces of code need to be added to our source code.
The first piece of code is to create a form where we'll be able to insert the new user information, as well collect this and submit the data to Supabase using React Query:
import { useForm } from 'react-hook-form';
import { useSignUp } from 'path/to/useSignUp';
// other imports
function SignupForm() {
const { register, formState, getValues, handleSubmit, reset } = useForm();
const { signUp, isCreatingUser } = useSignUp();
const { errors } = formState;
function onSubmit({ fullName, email, password }) {
signUp({ fullName, email, password }, { onSettled: () => reset() });
}
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<FormRow label='Full name' error={errors?.fullName?.message}>
<Input
disabled={isCreatingUser}
type='email'
id='email'
{...register('email', {
required: 'This field is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Please provide a valid e-mail address',
},
})}
/>
</FormRow>
<FormRow
label='Password (min 8 characters)'
error={errors?.password?.message}
>
<Input
disabled={isCreatingUser}
type='password'
id='password'
{...register('password', {
required: 'This field is required',
minLength: {
value: 8,
message: `The password should be at least 8 characters`,
},
})}
/>
</FormRow>
{/* other form elements go here*/}
</Form>
);
}
export default SignupForm;
After the form is created, we need to add a sign up function to the apiAuth.js
and make use of that in a custom hook (useSignUp.js
) in order to connect the newly created form to Supabase using React Query:
// file apiAuth.js
import supabase from 'path/to/supabase';
export function signUp({ fullName, email, password }) {
const { data, error } = supabase.auth.signUp({
email,
password,
// this "options" object can be passed in order to provide additional data to be added to a new user upon creation
//those values should be provided by the form where we create the user
options: {
data: {
fullName,
avatar: '',
},
},
});
if (error) {
console.error(error);
throw new Error(error.message);
}
return data;
}
// file useSignUp.js
import { useMutation } from '@tanstack/react-query';
import { signUp as signUpApi } from 'path/to/apiAuth';
export function useSignUp() {
const { mutate: signUp, isLoading: isCreatingUser } = useMutation({
mutationFn: ({ fullName, email, password }) =>
signUpApi({ fullName, email, password }),
onSuccess: (data) => {
// definitions on how to proceed on a successful account creation
},
onError: (err) => {
// definitions on how to process on in case of errors
},
});
return { signUp, isCreatingUser };
}
With this, the application should be ready to go, with the whole authorization and authentication handled correctly.
To protect our database access, preventing data tampering, we need to set strict RLS. In order to do so, the database operations DELETE, INSERT, SELECT and UPDATE should be set on the Policies section of the Authentication menu to autenticated for all tables which needs protection.
In case your API needs to provide some public date for a external website, or for example a homepage from which some users need to get basic information (lets imagine an user trying to check prices of items in a online store), the SELECT operation has to be set to public, on the same menu.
In order to update the users data, first we need a form which will be used to to colect the data which will be passed to Supabase. The componets should be created in a similar way to the previously form created in this section in the SignUpForm.jsx and no code for those will be provided here.
The methods for connecting the form to Supabase using React Query are also something similar to what's already done, so here we are just going to add the portions of code which are really relevant for the user update step, and not the whole process / code itself.
// file apiAuth.js
// imports and other definitions
export async function updateCurrentUser({ password, fullName, avatar }) {
let updateData;
if (password) updateData = { password };
if (fullName) updateData = { data: { fullName } };
const { data: updateName, error: updateNameError } =
await supabase.auth.updateUser(updateData);
if (updateNameError) {
console.error(updateNameError);
throw new Error(updateNameError.message);
}
if (!avatar) return updateName;
const fileName = `avatar-${updateName.user.id}-${Math.random()}`;
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, avatar);
if (uploadError) {
console.error(uploadError);
throw new Error(uploadError.message);
}
const { data: updateAvatar, error: updateAvatarError } =
await supabase.auth.updateUser({
data: {
avatar: `${supabaseUrl}/storage/v1/object/public/avatars/${fileName}`,
},
});
if (updateAvatarError) {
console.error(updateAvatarError);
throw new Error(updateAvatarError.message);
}
return updateAvatar;
}
// file useUpdateUser.js
import { useMutation, useQueryClient } from '@tanstack/react-query';
//other imports
import { updateCurrentUser as updateCurrentUserApi } from 'path/to/apiAuth';
export function useUpdateUser() {
const queryClient = useQueryClient();
const { mutate: updateCurrentUser, isLoading: isUpdating } = useMutation({
mutationFn: updateCurrentUserApi,
onSuccess: () => {
queryClient.invalidateQueries(['user']);
// other success handling functions here
},
onError: (err) => {
// error handling methods here
},
});
return { updateCurrentUser, isUpdating };
}
As part of the requirements of the project, we have to display some specific data as charts on our dashboard. For that, in our tach-stack, it was defined that we'd use the Recharts 3rd-party library.
So, in order to make use of this, first we need to install it using:
# install a specific version
npm i recharts@<version_number>
# install latest version
npm i recharts@latest
Then in order to create some charts on the application we need to import and setup the chart just like shown in the snippet of code below, importing all the dependencies from recharts to the component file where the chart is being defined:
<ResponsiveContainer>
<AreaChart data={dataToRender} width='100%' height='height-value'>
<CartesianGrid strokeDasharray='4' />
<Tooltip contentStyle={{ backgroundColor: 'tooltip-backgroun' }} />
<XAxis
dataKey='labelToRender'
tick={{
fill: 'fill-color',
}}
tickLine={{
stroke: 'stroke-color',
}}
/>
<YAxis
tick={{
fill: 'fill-color',
}}
tickLine={{
stroke: 'stroke-color',
}}
/>
<Area
type='monotone'
dataKey='dataToRender'
stroke='stroke-color'
fill='fill-color'
strokeWidth={2}
name='Data name'
/>
</AreaChart>
</ResponsiveContainer>
For other chart types and definitions, check the documentation.
It's completly normal that some bugs may occasionaly appear in production. So, that means that every time that during development we see a white screen due to some rendering error, the users of the application should also see.
Of course this is not the desired behaviour, so we can fix these kind of bugs by using a React feature called Error Boundaries, which are like try-catch for for renders, which allows React to "react" to errors in render logic.
Error boundries are though, still very hard to use, because they are writting as class components and into a very weird and hard to use way.
Gladly, there is a 3rd-party library called react-error-boundary which makes the implementation of this feature something easy.
To make use of that we need to install it using the command below:
npm i react-error-boundary
The way it works is by warpping the entire application inside the returned component provided by the library, just like in the code below:
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary';
import App from './App.jsx';
import ErrorFallback from 'path/to/ErrorFallback.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => window.location.replace('/')}
>
<App />
</ErrorBoundary>
</React.StrictMode>
);
Where this ErrorFallback component should be something similar to:
// ErrorFallbackComponent.jsx
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<h1>Something went wrong</h1>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export default ErrorFallback;