Table of content


useEffect

intro

useEffect(() => { const timer = setTimeout(() => { // change }, 1500); return clearTimer(timer); }, [text]);

it has 3 parts:

use cases:


useReducer()

intro

parts

reducer function that take old state and action then return new state.

const countReducer = (prevState , action) => { switch(action.type){ case INC: return {value: prevState.value + 1 }; case DEC: return {value: prevState.value - 1 }; case CLEAR: return {value: 0 }; } return { value: 0}; }

use

const [count, countDispatch] = useReducer(countReducer , {value: 0});

countDispatch used to send action to reducer function:

countDispatch({type: INC});

Context API

intro

it is for state managment for component and accros app. We create store file with context. it hold object.

creating it

const AuthContext = React.createContext({ isLoggedIn: false });

using it

the use by hook.

const ctx = useContext(AuthContext); ctx.isLoggedIn // get the value.

will need AuthContext.Provider to wrap component that will use context,

return ( <AuthContext.Provider value={{ isLoggedIn: isLoggedIn, onLogout: logoutHandler, onLogin: loginHandler, }} > <App /> </AuthContext.Provider> ); };

advanced use

it is advised to be like:

export const AuthContextProvider = (props) => { const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { const storedUserLoggedInInformation = localStorage.getItem('isLoggedIn'); if (storedUserLoggedInInformation === '1') { setIsLoggedIn(true); } }, []); const logoutHandler = () => { localStorage.removeItem('isLoggedIn'); setIsLoggedIn(false); }; const loginHandler = () => { localStorage.setItem('isLoggedIn', '1'); setIsLoggedIn(true); }; return ( <AuthContext.Provider value={{ isLoggedIn: isLoggedIn, onLogout: logoutHandler, onLogin: loginHandler, }} > {props.children} </AuthContext.Provider> ); };

Cons


Class based componenets

use cases

Use

will use this to access props.

import { Component } from 'react'; import classes from './User.module.css'; class User extends Component { constructor() { super(); // must call it this.state = { filteredUsers: [], searchTerm: '', }; } // Called after a component is mounted componentDidMount() { // this is the way we update state this.setState({ filteredUsers: this.context.users }); } // when component re-evaluted componentDidUpdate(prevProps, prevState) { if (prevState.searchTerm !== this.state.searchTerm) { this.setState({ filteredUsers: this.context.users.filter((user) => user.name.includes(this.state.searchTerm) ), }); } } // ummounted from screen. componentWillUnmount() { console.log('User will unmount!'); } render() { return <li className={classes.user}>{this.props.name}</li>; } } export default User;

Error Boundary

code:

import { Component } from 'react'; class ErrorBoundary extends Component { constructor() { super(); this.state = { hasError: false }; } // Catches exceptions generated in descendant components. componentDidCatch(error) { console.log(error); this.setState({ hasError: true }); } render() { if (this.state.hasError) { return <p>Something went wrong!</p>; } return this.props.children; } } export default ErrorBoundary;

use context

define contextType and assign it to context. it is allowed only one context to be used.

static contextType = UsersContext;

Redux

intro

use

add it to project. npm install redux react-redux

create store at a new folder.

import { createStore } from 'redux'; const counterReducer = (state = { counter: 0 }, action) => { if (action.type === 'increment') { return { counter: state.counter + 1, }; } if (action.type === 'decrement') { return { counter: state.counter - 1, }; } return state; }; const store = createStore(counterReducer); export default store;

then wrap App or root component.

import Provider from "react-redux"; root.render( <Provider store={appStore}> <App /> </Provider> );

to get the state and its data, we use selector to define which part is important for us for watching.

// state: represent the whole store state. const value = useSelector(state => state.value);

to send actions, we get the dispatcher by:

const dispatch = useDispatch(); // then use it dispatch({ type: 'decrement' }); // you can add any data to action and use it at reducer

for class based components, u will need some mapping

import {connect} from 'react-redux' // at handler call `this.props.increment();` // to get data call `this.props.counter` const mapStateToProps = state => { return { counter: state.counter }; } const mapDispatchToProps = dispatch => { return { increment: () => dispatch({ type: 'increment' }), decrement: () => dispatch({ type: 'decrement' }), } }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);

NOTE: new object from reducer is overrdiing old state not merging it, i.e if miss a value, it will be earsed.

use toolkilt

installing

npm install @reduxjs/toolkit

single slic, simple actions

slic contains a seperated state and its actions.

import { createSlice, configureStore } from '@reduxjs/toolkit'; const initialState = { counter: 0, showCounter: true }; const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment(state) { state.counter++; }, decrement(state) { state.counter--; }, increase(state, action) { state.counter = state.counter + action.payload; }, toggleCounter(state) { state.showCounter = !state.showCounter; } } }); const store = configureStore({ reducer: counterSlice.reducer }); export const counterActions = counterSlice.actions; export default store;

NOTES:

instead of having actions hard coder, now call it as function that will return the hard coded one.

dispatch(counterActions.increment()); // counterActions exported before from slic using `slic.actions`.

multiple slic

create slices then merge them to be one reducer

const store = configureStore({ reducer: {counter:counterSlice.reducer , auth:authSlice.reducer} });

when calling it at useSelector use its name as example counter:

const counter = useSelector((state) => state.auth.isLogged);

using tunks to add async to actions.

tunk: is function that return a function that being delayed. normal action creator return action like {type: 'inc', payload:5} let see in action.

export const sendCartData = (cart) => { return async (dispatch) => { dispatch( uiActions.showNotification({ status: 'pending', title: 'Sending...', message: 'Sending cart data!', }) ); const sendRequest = async () => { const response = await fetch( 'https://react-http-6b4a6.firebaseio.com/cart.json', { method: 'PUT', body: JSON.stringify({ items: cart.items, totalQuantity: cart.totalQuantity, }), } ); if (!response.ok) { throw new Error('Sending cart data failed.'); } }; try { await sendRequest(); dispatch( uiActions.showNotification({ status: 'success', title: 'Success!', message: 'Sent cart data successfully!', }) ); } catch (error) { dispatch( uiActions.showNotification({ status: 'error', title: 'Error!', message: 'Sending cart data failed!', }) ); } }; };

NOTES:

the use: is like normal actions, dispatch(sendCartData(cart))


Authentication

intro

samples

logout action will be used with route of logout,

{ path: 'logout', action: logoutAction, },
export function action() { localStorage.removeItem('token'); return redirect('/'); }

logout button, when clicked a form will submit a post request to action of logout route

<Form action="/logout" method="post"> <button>Logout</button> </Form>

at action of login form we can get params by:

export async function action({ request }) { const searchParams = new URL(request.url).searchParams; const mode = searchParams.get('mode') || 'login'; ..... return redirect('/'); // return to home page. }

Deployement

lazy loading

for component

const BlogPage = lazy( () => import('./pages/BlogPage') ); element: ( <Suspense fallback={<p>Loading...</p>}> <BlogPage /> </Suspense> ),

for loader

{ index: true, element: ( <Suspense fallback={<p>Loading...</p>}> <BlogPage /> </Suspense> ), loader: () => import('./pages/Blog').then((module) => module.loader()), },

client vs server routing

as we now React, is single page which means only one page is loaded from server then React do routing so we must configure our server to treat it as single page, on firebase it do this easily.


Animation

using css

we can use transition and keyframes to add animation. At first, we should add transition to enable it

.Model { transition: all 0.3s ease-out; }
.ModalOpen { animation: openModal 0.4s ease-out forwards; } .ModalClosed { animation: closeModal 0.4s ease-out forwards; } @keyframes openModal { 0% { opacity: 0; transform: translateY(-100%); } 50% { opacity: 1; transform: translateY(90%); } 100% { opacity: 1; transform: translateY(0); } } @keyframes closeModal { 0% { opacity: 1; transform: translateY(0); } 50% { opacity: 0.8; transform: translateY(60%); } 100% { opacity: 0; transform: translateY(-100%); } }

then at code, we add class based on state.

const modal = props => { const cssClasses = [ "Modal", props.show ? "ModalOpen" : "ModalClosed" ]; return ( <div className={cssClasses.join(' ')}> <h1>A Modal</h1> <button className="Button" onClick={props.closed}> Dismiss </button> </div> ); };

use react-transition-group

why we need it?

it fix the lack of css transition which when compoent is being unmounted from screen it may cut the animation i.e not run that animation properly as compoent is leaving the screen which may lead to a bad UX.

Use

at first install it, npm install react-transition-group for normal compoent, we will use Transition,

import Transition from "react-transition-group/Transition";

wrap it to what you need to transition

<Transition in={this.state.showBlock} timeout={1000} mountOnEnter unmountOnExit> {state => ( <div style={{ backgroundColor: "red", width: 100, height: 100, margin: "auto", transition: 'opacity 1s ease-out', opacity: state === 'exiting' ? 0 : 1 }} /> )} </Transition>

Use CSSTransition

we can use CSSTransition component, and with classNames add different class to be added. it can be configured by two ways

<CSSTransition mountOnEnter unmountOnExit in={props.show} timeout={animationTiming} classNames={{ enter: '', enterActive: 'ModalOpen', exit: '', exitActive: 'ModalClosed' }}> <div className="Modal"> <h1>A Modal</h1> <button className="Button" onClick={props.closed}> Dismiss </button> </div> </CSSTransition>

TransitionGroup

if you want to transition a list we can use TransitionGroup.

<TransitionGroup component="ul" className="List"> {listItems} </TransitionGroup>

then each item must be a Transition item

<CSSTransition key={index} classNames="fade" timeout={300}> <li className="ListItem" onClick={() => this.removeItemHandler(index)}> {item} </li> </CSSTransition>

you may notice that in doesnot added here, as this is managed by TransitionGroup. as sample, you will add different classes of css like

.fade-enter { opacity: 0; }

Other libs

there are many js library for animation like react-motion


Testing

we will test our component and logic by using 3 A's

we will need react testing library and jest which installed by default with create-react-app.

defining suit and first test

we create files that ends with *.test.js.

import { render, screen } from '@testing-library/react'; import Greeting from './Greeting'; describe('Greeting component', () => { test('renders Hello World as a text', () => { // Arrange render(<Greeting />); // Act // ... nothing // Assert const helloWorldElement = screen.getByText('Hello World!'); expect(helloWorldElement).toBeInTheDocument(); }); });

adding mocks

this is helpful to make our tests robust and not fails over time. jest will be used. a sample to mock fetch method:

import { render, screen } from '@testing-library/react'; import Async from './Async'; describe('Async component', () => { test('renders posts if request succeeds', async () => { window.fetch = jest.fn(); window.fetch.mockResolvedValueOnce({ json: async () => [{ id: 'p1', title: 'First post' }], }); render(<Async />); const listItemElements = await screen.findAllByRole('listitem'); expect(listItemElements).not.toHaveLength(0); }); });

React & TypeScript

Basics

let userName: string | string[]; type Person = { name: string; age: number; }; function insertAtBeginning<T>(array: T[], value: T) { const newArray = [value, ...array]; return newArray; }

React

it can be added from start with createReactApp by adding --template typescript it comes with some types and utilities

defining models for data (we can use interface):

class Todo { id: string; text: string; constructor(todoText: string) { this.text = todoText; this.id = new Date().toISOString(); } } export default Todo;

for forms and refs:

const todoTextInputRef = useRef<HTMLInputElement>(null); const submitHandler = (event: React.FormEvent) => { event.preventDefault(); const enteredText = todoTextInputRef.current!.value; if (enteredText.trim().length === 0) { // throw an error return; } };

for compoents:

const TodoItem: React.FC<{ text: string }> = (props) => { return <li>{props.text}</li>; }; export default TodoItem;

with sates:

const [todos, setTodos] = useState<Todo[]>([]);

with context:

React.createContext<TodosContextObj>