useEffect(() => {
const timer = setTimeout(() => {
// change
}, 1500);
return clearTimer(timer);
}, [text]);
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};
}
const [count, countDispatch] = useReducer(countReducer , {value: 0});
countDispatch
used to send action to reducer function:
countDispatch({type: INC});
it is for state managment for component and accros app. We create store file with context. it hold object.
const AuthContext = React.createContext({
isLoggedIn: false
});
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>
);
};
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>
);
};
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;
componentDidCatch
to get notfified when error occured.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;
define contextType
and assign it to context.
it is allowed only one context to be used.
static contextType = UsersContext;
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.
npm install @reduxjs/toolkit
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:
state.counter--;
not mutate old state but toolkit use a library to create a merged version for us.action.payload
extra data sent with action will be stored here.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`.
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);
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:
return async (dispatch)
.(dispatch)
that can be used to dispatch other actionsthe use: is like normal actions,
dispatch(sendCartData(cart))
loader
to main root, inwhich we will return auth data to be avaiable to all subroutes.action
to redirect user to home/auth screen after submit logout.useSearchParams()
to get search params in current active page.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.
}
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()),
},
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.
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;
}
all
: specify all properties will be transitioned, we can specify opacity as sample0.3s
time of transitionease-out
: interprolation method [how transition will be done].
then we can use key frames like next
.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%);
}
}
animation
to define the transition key frames with timing and function.keyframes
we specify different values based on current progress of transition.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>
);
};
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.
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>
in
: Show the component, triggers the enter or exit states.[boolean]mountOnEnter
, unmountOnExit
: used to auto add/remove compoent from tree with changes in states.timeout
: timing of transition. can be single number or object like: timeout={{
appear: 500,
enter: 300,
exit: 500,
}}
onEnter
,onEntering
,onEntered
,onExit
,onExiting
,onExited
] {state => ()}
we can use CSSTransition component, and with classNames
add different class to be added.
it can be configured by two ways
classNames="fade"
and it will search for all different class values like fade-enter
, fade-enter-active
,
fade-exit
, fade-exit-active
, fade-appear
, and fade-appear-active
.<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>
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;
}
there are many js library for animation like react-motion
we will test our component and logic by using 3 A's
Arrange
: prepare code for testing like render componentsAct
: doing action like clicking buttonsAssert
: verfigy and check some condition like element exist on screenwe will need react testing library and jest which installed by default with create-react-app
.
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();
});
});
describe(name, func)
: used to define a suit,it needs a title, and a fuction that will have all teststest(name, func)
: write our test inside itgetByText()
helper method to get element to be asserted.findByRole()
to test async code.expect()
: used to assert, also there is a bunch of assertion we can use.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);
});
});
fn()
used to return a mocked versionmockResolvedValueOnce()
to return the value.
for more check jest , roleslet userName: string | string[];
type Person = {
name: string;
age: number;
};
function insertAtBeginning<T>(array: T[], value: T) {
const newArray = [value, ...array];
return newArray;
}
it can be added from start with createReactApp by adding --template typescript
it comes with some types and utilities
bind(null, id)
to bind first paramater of a method with id, null
used to this
.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:
event: React.FormEvent
so preventDefault
is known now.!
added to tell ts that we make sure we have value here, as it do not know at that place we are sure to have value we would use ?
to be in safe. 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:
React.FC
to tell it is functional compoent so we have childern
const TodoItem: React.FC<{ text: string }> = (props) => {
return <li>{props.text}</li>;
};
export default TodoItem;
with sates:
useState<Todo[]>
.const [todos, setTodos] = useState<Todo[]>([]);
with context:
React.createContext<TodosContextObj>