GET https://api.chucknorris.io/jokes/random
// create a new project
npx create-react-app react-playground --template typescript
// install axios
npm i axios
npm start
import { useRef } from 'react';
function Component() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current?.focus();
}
return <div>
<input ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</div>
}
Parent
Child
do something
Parent
Child
do something
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => inputRef.current?.focus();
return <>
<Child ref={inputRef} />
<button onClick={handleClick}>
Focus
</button>
</>
}
const Child = forwardRef<HTMLInputElement>((props, ref) => {
return <div>
<input ref={ref} />
</div>
})
function Parent() {
const childApiRef = useRef<{ focus: () => void }>(null);
const handleClick = () => childApiRef.current?.focus();
return <>
<Child ref={childApiRef} />
<button onClick={handleClick}>
Focus
</button>
</>
}
const Child = forwardRef<{ focus: () => void }>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}), [inputRef])
return <div>
<input ref={inputRef} />
</div>
})
import { useState } from "react";
import { AlbumItem } from "./AlbumItem";
export type Album = {
id: number;
title: string;
rating: number;
}
export const AlbumVoting = () => {
const [albums, setAlbums] = useState(new Array(15).fill({
id: 0,
title: '',
rating: 0
}).map((album, index) => ({ ...album, id: index })));
function handleChange (updatedAlbum: Album) {
setAlbums(albums => albums.map(album => album.id === updatedAlbum.id ? updatedAlbum : album));
};
return <div>
{albums.map(album => <AlbumItem key={album.id} album={album} onChange={handleChange} />)}
</div>
}
import { Album } from "./AlbumVoting";
type Props = {
album: Album,
onChange: (updated: Album) => void
}
export const AlbumItem = ({ album, onChange }: Props) => {
function handleChange(change: Partial<Album>) {
onChange({
...album,
...change
});
}
return <p>
Title:
<input value={album.title}
onChange={(e) => handleChange({ title: e.target.value })}
/>
Rating
<input type='number'
value={album.rating}
onChange={(e) => handleChange({ rating: Number(e.target.value) })}
/>
</p>
}
AlbumVoting.tsx
AlbumItem.tsx
const ExpensiveComponentMemoized = React.memo(function ExpensiveComponent() {
...
});
<ExpensiveComponentMemoized />
const useFibonacci = (n) => {
const result = useMemo(() => fibonacci(n), [n]);
return result;
}
function fibonacci(n) {
return n < 1 ? 0
: n <= 2 ? 1
: fibonacci(n - 1) + fibonacci(n - 2)
}
type Props = {
me: string;
}
function Component({me}: Props) {
const handleClick = useCallback(
(name) => console.log(`Hello ${name} and ${me}`)
, [me]);
return <ExpensiveComponent onClick={handleClick} />;
}
import { useState } from "react";
import { AlbumItem } from "./AlbumItem";
export type Album = {
id: number;
title: string;
rating: number;
}
export const AlbumVoting = () => {
const [albums, setAlbums] = useState(new Array(15).fill({
id: 0,
title: '',
rating: 0
}).map((album, index) => ({ ...album, id: index })));
function handleChange (updatedAlbum: Album) {
setAlbums(albums => albums.map(album => album.id === updatedAlbum.id ? updatedAlbum : album));
};
return <div>
{albums.map(album => <AlbumItem key={album.id} album={album} onChange={handleChange} />)}
</div>
}
import { Album } from "./AlbumVoting";
type Props = {
album: Album,
onChange: (updated: Album) => void
}
export const AlbumItem = ({ album, onChange }: Props) => {
function handleChange(change: Partial<Album>) {
onChange({
...album,
...change
});
}
return <p>
Title:
<input value={album.title}
onChange={(e) => handleChange({ title: e.target.value })}
/>
Rating
<input type='number'
value={album.rating}
onChange={(e) => handleChange({ rating: Number(e.target.value) })}
/>
</p>
}
AlbumVoting.tsx
AlbumItem.tsx
type ContextValue = boolean;
const MyContext = React.createContext<ContextValue>(false);
function App() {
return <MyContext.Provider value={true}>
<Component />
</MyContext.Provider>;
}
function Component() {
const value = useContext(MyContext);
return <div>{value}</div>;
}
type ContextValue = {
value: number;
setValue: (value: ContextValue['value']) => void
};
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
function App() {
const [value, setValue) = useState(0);
return <MyContext.Provider value={{ value, setValue}}>
<Component />
</MyContext.Provider>;
}
function Component() {
const {value, setValue} = useContext(MyContext);
return <>
<div>{value}</div>
<button onClick={() => setValue(value + 1)}>Click</button>
</>;
}
type ContextValue = {
value: number;
setValue: (value: ContextValue['value']) => void
};
// not exported - private
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
export function MyContextProvider({children}: PropsWithChildren) {
const [value, setValue) = useState(0);
return <MyContext.Provider value={{ value, setValue}}>
{children}
</MyContext.Provider>;
}
// only way to read the value:
export const useMyContext = () => useContext(MyContext);
// in another file
import {useMyContext} from './my-context';
function Component() {
const { value, setValue } = useMyContext();
return <>
<div>{value}</div>
<button onClick={() => setValue(value + 1)}>Click</button>
</>;
}
<MyContextProvider>
<Component />
</MyContextProvider>
type ContextValue = {
value: number;
changeValue: (newValue: ContextValue['value']) => void;
}
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
type Props = {
initialState: ContextValue['value'];
children: React.ReactNode
}
export function MyContextProvider({initialState, children}: Props) {
const [value, setValue] = useState(initialState);
return <MyContext.Provider value={{value, setValue}}>
{children}
</MyContext.Provider>;
}
<MyContextProvider initialState={5}>
...
</MyContextProvider>
export function UserInfoPane() {
const { user } = useUser();
return <div>
{user ? <CurrentUserInfo /> : <LoginForm />}
</div>
}
export function LoginForm() {
const { login } = useUser();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
return <div>
<input placeholder="username" value={username} onChange={(e) => setUsername(e.target.value)} />
<input placeholder="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button onClick={() => login({username, email})}>Login</button>
</div>
}
type UserContext = {
user: User | null,
login: (user: User) => void;
logout: () => void;
}
const useUser = () => useContext(UserContext);
export function UserInfoPane() {
const { user } = useUser();
return <div>
{user ? <CurrentUserInfo /> : <LoginForm />}
</div>
}
export function LoginForm() {
const { login } = useUser();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
return <div>
<input placeholder="username" value={username} onChange={(e) => setUsername(e.target.value)} />
<input placeholder="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button onClick={() => login({username, email})}>Login</button>
</div>
}
type UserContext = {
user: User | null,
login: (user: User) => void;
logout: () => void;
}
UserContextProvider
UserInfoPane
CurrentUserInfo
LoginForm
= missing
const useUser = () => useContext(UserContext);
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})
import {add} from './add';
test('adding two numbers', () => {
expect(add(1, 3)).toBe(4);
})
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})
import {add} from './add';
it('adds numbers', () => {
// Arrange
const a = 1;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(4);
})
expect(add(1, 3)).toBe(4);
subject
matcher
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).not.toBe(5);
})
expect('How are you?').toEqual(expect.stringContaining('How'));
expect({a: 1, b: 2}).toBe({a: 1, b: 2}); // ❌ DON'T
expect({a: 1, b: 2}).toEqual({a: 1, b: 2}); // ✅ DO
expect({a: 1, b: 2}).toEqual(expect.objectContaining({a: 1}));
expect({a: 1, b: 2}).toHaveProperty('a', 1);
expect({
one: 1,
two: {
nested: 2
}
}).toHaveProperty('two.nested', 2);
expect([1,2,3]).toBe([1,2,3]); // ❌
expect([1,2,3]).toEqual([1,2,3]); // ✅
expect([1,2,3]).toEqual(expect.arrayContaining([1,2]));
expect(testedFn()).toThrow(); // ❌ DON'T
expect(() => testedFn()).toThrow(); // ✅ DO
function doNTimes(fn, n) {
for(let i = 0 ; i < n; i++) {
fn();
}
}
// ❌ DON'T - better to use mocks (later)
it('calls the function n times', () => {
expect.assertions(3);
function fn() {
expect(true).toBe(true);
}
doNTimes(fn, 3)
})
{
"name": "...",
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!(axios))"
]
},
...
}
test.skip('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
npm i -D @babel/plugin-proposal-private-property-in-object
npm test
export function divideWithRemainder(a: number, b: number) {
if (b === 0) {
throw new Error('Cannot divide by zero.');
}
const result = Math.ceil(a / b);
const remainder = a % b;
return {
result,
remainder
};
}
const fn = jest.fn(); // empty function
// pass implementation
const fn = jest.fn(() => {
return 5;
});
// override implementation
fn.mockImplementation(() => {
return 6;
});
fn.mockReturnValue(5);
fn.mockResolvedValue(5); // returns promise which resolves in 5
fn.mockReturnValueOnce(1);
fn.mockReturnValueOnce(2);
fn.mockReturnValue(3);
fn() // 1
fn() // 2
fn() // 3
fn() // 3
const fn = jest.fn(() => 42); // empty function
expect(fn).toHaveBeenCalled(); // fails
fn();
expect(fn).toHaveBeenCalled(); // passes
expect(fn).toHaveBeenCalledTimes(1);
fn(1,2,3)
expect(fn).toHaveBeenLastCalledWith(1,2,3);
expect(fn).toHaveReturnedWith(42); // test output
function doNTimes(fn, n) {
for(let i = 0 ; i < n; i++) {
fn();
}
}
jest.spyOn(Math, 'random');
Math.random();
expect(Math.random).toHaveBeenCalled();
// change behavior
const randomMock = jest.spyOn(Math, 'random');
randomMock.mockReturnValue(1);
expect(Math.random()).toBe(1);
import { fetchUser } from './fetch-user';
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
it('fetches user data', async () => {
const response = { data: { id: 1, name: 'Martin' } };
mockedAxios.get.mockResolvedValue(response);
expect(await fetchUser()).toEqual({ id: 1, name: 'Martin' });
});
import axios from 'axios';
function fetchUser() {
return axios.get('/url').then((response) => response.data);
}
Type of Query | 0 Matches | 1 Match | >1 Matches | Retry (Async/Await) |
---|---|---|---|---|
Single Element | ||||
getBy... | Throw error | Return element | Throw error | No |
queryBy... | Return null | Return element | Throw error | No |
findBy... | Throw error | Return element | Throw error | Yes |
Multiple Elements | ||||
getAllBy... | Throw error | Return array | Return array | No |
queryAllBy... | Return [ ] | Return array | Return array | No |
findAllBy... | Throw error | Return array | Return array | Yes |
import { screen } from '@testing-library/react';
screen.getByRole('button', {name: /submit/i});
getByRole('button', { name: 'Submit' });
// // checked input type checkbox
getByRole('checkbox', { checked: true });
// usually h2
getByRole('heading', { level: 2 });
screen.getByText('Hello World'); // full string match
screen.getByText('llo Worl', {exact: false}); // substring match
screen.getByText('hello world', {exact: false}); // ignore case
screen.getByText(/World/); // regexp
// custom function
screen.getByText((content, element) => content.startsWith('Hello'));
<div class="background red" data-testid="background">
...
</div>
const backgroundEl = screen.getByTestId('background');
import { screen, within } from '@testing-library/react';
const container = screen.getByTestId('container');
const helloMessage = within(container).getByText('hello');
waitForElementToBeRemoved
)import { screen, waitFor } from '@testing-library/react';
await waitFor(() => {
expect(screen.getByText('Some text')).toBeInTheDocument();
});
// click on an element
fireEvent.click(screen.getByText('Login'));
// change input value
fireEvent.change(getByLabelText(/username/i), {target: {value: 'martin'}});
// check text content
const element = screen.getByTestId('title');
expect(element).toHaveTextContent('This is title');
// check if is in the document
const element = screen.queryByText('Submit');
expect(element).toBeInTheDocument();
// test focus
const input = screen.getByTestId('password');
expect(input).toHaveFocus();
// test checkbox state
const rememberPass = screen.getByRole('checkbox', {name: 'Remember password'});
expect(rememberPass).toBeChecked();
import { render } from '@testing-library/react';
it('renders', () => {
render(<App />);
const element = screen.queryByText('Hello');
expect(element).toBeInTheDocument();
});
screen.debug()
screen.logTestingPlaygroundURL()
import { screen } from '@testing-library/react';
1. render -> check LoginForm is visible
{
"name": "...",
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!(axios))"
]
},
...
}
package.json
import { act, renderHook } from "@testing-library/react";
import { useCounter } from "./use-counter";
it('increments', () => {
const {result} = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
})
export function useCounter() {
const [count, setCount] = useState(0);
const increment =() => setCount(count + 1);
return { count, increment };
}
export function useCounter(initialValue: number) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const randomize = () => setCount(Math.random());
return { count, increment, randomize };
}
Children
<WrapperComponent>
</WrapperComponent>
function Component() {
return <MousePosition render={
(x, y) => <p>Position is x={x}, y={y}</p>
} />
}
type Props = {
render: (x: number, y: number) => React.ReactNode;
}
function MousePosition({ render }: Props) {
...
return <>{render(position.x, position.y)}</>;
}
export function MyComponent() {
function setWallet(n: number) { /* ... */ }
return (
<DropdownButton label="Menu">
<button onClick={() => setWallet(10)}>+10</button>
<button onClick={() => setWallet(30)}>+30</button>
<button onClick={() => setWallet(50)}>+50</button>
</DropdownButton>
);
}
export function DropdownButton({ label, children }: PropsWithChildren<Props>) {
const [isOpen, setIsOpen] = useState(false);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setIsOpen(!isOpen)}>{label}</button>
{isOpen && (
<div
style={{
position: "absolute",
background: "white",
padding: "10px",
border: "1px solid black",
}}
>
{children}
</div>
)}
</div>
);
}
export function MyComponent() {
function setWallet(n: number) { /* ... */ }
return (
<DropdownButton label="Menu">
{({close}) => <>
<button onClick={() => setWallet(10)}>+10</button>
<button onClick={() => setWallet(30)}>+30</button>
<button onClick={() => setWallet(50)}>+50</button>
<button onClick={close}>X</button>
</>}
</DropdownButton>
);
}
type DropdownApi = {
close: () => void;
};
type Props = {
label: string;
children: (api: DropdownApi) => React.ReactNode;
};
export function DropdownButton({ label, children }: Props) {
const [isOpen, setIsOpen] = useState(false);
function close() {
setIsOpen(false);
}
return (
<div style={{ position: "relative" }}>
<button onClick={() => setIsOpen(!isOpen)}>{label}</button>
{isOpen && (
<div
style={{
position: "absolute",
background: "white",
padding: "10px",
border: "1px solid black",
}}
>
{children({ close })}
</div>
)}
</div>
);
}
<ErrorBoundary>
<Joke />
</ErrorBoundary>
import React, { PropsWithChildren } from "react";
type Props = PropsWithChildren;
type State = {
hasError: boolean;
error?: unknown;
}
export class JokeErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: unknown) {
return { hasError: true, error };
}
// optional
componentDidCatch(error: unknown, errorInfo: ErrorInfo) {
console.error(error, errorInfo);
}
render() {
const {error, hasError} = this.state;
if (hasError) {
// fallback
return <div>{String(error)}</div>;
}
return this.props.children;
}
}
export function ThrowComponent() {
if (Math.random() < 0.5) {
throw 'Something went wrong.'
};
return <p>Didn't throw</p>
}
Routable pages
Navbar
import { Outlet } from "react-router";
export function Layout() {
return <div>
<NavBar />
<Outlet />
</div>
}
Here other routes render
Routable pages
<Outlet />
<Navbar />
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
... // (later)
]);
function App() {
return (
<RouterProvider router={router} />
);
}
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />
},
{
path: 'about',
element: <About />
},
{
path: 'articles',
element: <ArticlesContainer />,
children: [
{
path: ':articleId',
element: <Article />
}
{
path: ':articleId/comments',
element: <ArticleComments />
}
]
}
]
}
]);
Must have <Outlet /> inside
Layout component
Routable pages
Navbar
<Outlet>
Routable pages
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/articles">Articles</Link>
<Link to="/articles/{articleId}">Specific article</Link>
const navigate = useNavigate();
navigate('/articles');
import { useParams } from "react-router-dom";
function Joke() {
const params = useParams();
return (
{params.category}
);
}
/path-segment/:pathParam/something?query1=one&query2=two
useSearchParams
useParams
// joke.tsx
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
type JokeResponse = {
value: string;
};
export function Joke() {
const [joke, setJoke] = useState<string | null>(null);
const { category } = useParams();
useEffect(() => {
const queryParams = new URLSearchParams();
if (category) {
queryParams.set('category', category);
}
axios<JokeResponse>(
`https://api.chucknorris.io/jokes/random?${queryParams.toString()}`
).then((response) => setJoke(response.data.value));
}, [category]);
return <p>{joke}</p>;
}
// joke-categories.tsx
import axios from "axios";
import { useEffect, useState } from "react";
export function JokeCategories() {
const [categories, setCategories] = useState<string[]>([]);
useEffect(() => {
axios<string[]>("https://api.chucknorris.io/jokes/categories").then(
(response) => setCategories(response.data)
);
}, []);
return <>
{/* TODO */}
</>;
}
// navbar.tsx
import { Link } from "react-router-dom"
export const NavBar = () => {
return <ul>
<Link to="/">Home</Link>
<Link to="/categories">Joke categories</Link>
</ul>
}
stale-while-revalidate
import useSWR from 'swr'
const fetcher = (...args) =>
fetch(...args).then(res => res.json());
function Joke() {
const { data, isValidating } = useSWR(
`https://api.chucknorris.io/jokes/random`,
fetcher
);
return (
<p>{isValidating ? 'Loading...' : data.value}</p>
);
}
import useSWR, {useSWRConfig} from 'swr'
const fetcher = (...args) =>
fetch(...args).then(res => res.json());
function Joke() {
const { data, isValidating } = useSWR(
`https://api.chucknorris.io/jokes/random`,
fetcher
);
const { mutate } = useSWRConfig();
return <div>
<button onClick={() => mutate(`https://api.chucknorris.io/jokes/random`)}>
Load next
</button>
<p>{isValidating ? 'Loading...' : data.value}</p>
</div>;
}
function Parent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ComponentWithLoading />
</Suspense>
)
}
const { data, isValidating } = useSWR(url, fetcher, { suspense: true });
const { data, isValidating } = useSWR(url, fetcher);
// default export
const ComponentLazy = React.lazy(
() => import('./ComponentDefaultExport')
);
// rename named export as default
const ComponentLazy = React.lazy(() =>
import('./ComponentNamedExport')
.then(({ComponentNamedExport}) => ({default: ComponentNamedExport}))
);
function Parent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ComponentLazy prop="value" />
</Suspense>
);
}
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [ {
path: 'about',
// key must be "Component"
lazy: () =>
import("./about").then(({ About }) => ({Component: About}))
},
]
}
]);
Must be under Component key
import {createPortal} from 'react-dom';
function Component() {
return <div>
{createPortal(<div>Hello</div>, document.body)}
</div>
}
<Modal>
<h1>Hello</h1>
<p>I am modal</p>
</Modal>
type Props = {
children: React.ReactNode[];
}
type ExpandedState = Record<number, boolean>;
export function Accordeon({ children }: Props) {
const [expandedState, setExpandedState] = useState<ExpandedState>({});
const toggleExpansionHandler = useCallback(
(index: number) => {
setExpandedState(expandedState => {
if (expandedState[index]) {
delete expandedState[index];
} else {
expandedState[index] = true;
}
return {
...expandedState,
};
});
},
[],
);
return <>
{React.Children.map(children, (child, index) => (
<div>
<button onClick={() => toggleExpansionHandler(index)}>
{expandedState[index] ? '➖' : '➕'}
</button>
{child && expandedState[index] && child}
</div>
))}
</>;
}
type State = number;
type Increment = { type: 'increment'; payload: number };
type Randomize = { type: 'randomize' };
type Action = Increment | Randomize;
function reducer(state: State, action: Action) {
switch (action.type) {
case "increment":
return state + 1;
case "randomize":
return Math.random();
default:
return state;
}
}
export function Counter() {
const [ state, dispatch ] = useReducer(reducer, 0);
return <div>
Counter: {state}
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>Increment</button>
<button onClick={() => dispatch({ type: 'randomize' })}>Randomize</button>
</div>
}
Initial state
type State = {
user: User | null;
timeLeft: number
}
type Action = LOGIN | LOGOUT | SESSION_TICK;
{
jokeSlice: {
currentJoke: "Chuck Norris can speak Braille.",
isLoading: false
},
counterSlice: {
count: 5
},
uiSlice: {
dropdownVisible: false
}
}
View
Reducer
Store
dispatch an action
update the store
view reads data from the store
increment = (state) => {
state.count += 1;
}
// /src/store/counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
export type CounterState = {
count: number;
};
const initialState: CounterState = {
count: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state, action) => {
state.count += 1;
}
},
});
export const { increment } = counterSlice.actions;
export const counterReducer = counterSlice.reducer;
// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { jokeReducer } from './jokeSlice';
import { counterReducer } from './counterSlice';
import { uiReducer } from './uiSlice';
export const store = configureStore({
reducer: {
jokeSlice: jokeReducer,
counterSlice: counterReducer,
uiSlice: uiReducer
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// index.tsx
import { store } from './store/store'
import { Provider } from 'react-redux'
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
import { useAppSelector } from '../store/hooks';
function Counter() {
const count = useAppSelector(state => state.counterSlice.count);
return <div>Current count: {count}</div>
}
import { useAppDispatch } from '../store/hooks';
import { increment } from "../store/counterSlice";
function IncrementButton() {
const dispatch = useAppDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
useEffect(() => {
const id = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
Hello {fullName}
</div>
}
function ChatInput() {
return <Button useData={useDataWithLogging} />
}
expect(displayName === name).toBe(true);
const Parent = () => {
const SecondChild = () => {
return <div> <SecondChild/> </div>
}
const Child = () => {
return <div> <SecondChild/> </div>
}
return (
<div>
<Child/>
</div>
)
}
// album-item.tsx
import type { Album } from "./album-voting";
...
// album-voting.tsx
import { AlbumItem } from "./album-item";
...
function MyInput({ ref }: Props) {
return <input ref={ref} />;
}
<MyInput ref={ref} />;
const UserContext = React.createContext({});
function UserContextProvider({ children }) {
return (
<UserContext value={...}>
{children}
</UserContext>
);
}
import {Expandable} from './Expandable';
async function Notes() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<ClientComponent key={note.id}>
<p note={note} />
</ClientComponent>
))}
</div>
)
}
// Server component
function EmptyNote () {
async function createNoteAction() {
// Server Function
'use server';
await db.notes.create();
}
return <Button onClick={createNoteAction}/>;
}
"use server";
export async function createNoteAction() {
await db.notes.create();
}
"use client";
import {createNote} from './actions';
function EmptyNote() {
return <Button onClick={createNote}/>;
}
function Joke() {
const response = use(axios('https://api.chucknorris.io/random'));
const joke = response.data.value;
return <p>{joke}</p>
}
function App() {
return <Suspense fallback="Loading">
<Joke />
</Suspense>
}
function List() {
const [state, setState] = useState([1, 2, 3]);
const [optimisticState, addOptimistic] = useOptimistic(
state,
(currentState, optimisticValue) => {
// merge and return new state
// with optimistic value
return [...currentState, optimisticValue]
}
);
function handleAddNew(n: number) {
addOptimistic(n);
axios.post('/new-value').then((stateFromServer) => setState(stateFromServer));
}
return <ul>
{optimisticState.map(value => <li>{value}</li>)}
<button onClick={() => handleAddNew(Math.random())}>add new</button>
</ul>
}
npm init -y
npm i react react-dom
npm i -D typescript \
webpack webpack-cli webpack-dev-server \
ts-loader @types/react @types/react-dom \
html-webpack-plugin
npx tsc --init
// Update tsconfig:
"jsx": "react"
"outDir": "./dist",
webpack.config.js
npx webpack serve // dev server
npx webpack // build
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
},
output: {
filename: 'main.bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new HtmlWebpackPlugin({template: './public/index.html'}),
],
};
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.optimize.SplitChunksPlugin({minSize: 1}),
new HtmlWebpackPlugin({template: './public/index.html'}),
]
};