Started react section

This commit is contained in:
2024-12-10 21:47:44 +01:00
parent 2c05f038da
commit 5ae50d9381
10 changed files with 868 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ export default {
"/server/": require("../server/sidebar.json"),
"/aws/": require("../aws/sidebar.json"),
"/go": require("../go/sidebar.json"),
"/react": require("../react/sidebar.json"),
"/": [
{
text: "Home",

View File

@@ -1,3 +1,5 @@
# Frameworks
[Nuxtjs](/nuxt/)
[React](/react/)

99
docs/react/components.md Normal file
View File

@@ -0,0 +1,99 @@
# Components
Components are the building blocks of a React application. They contained logic and templates that are reused throughout your applications. Components are essentially functions that return some JSX that can then be imported and used by parent components.
```tsx
export default function Component() {
return <h1>Some component!</h1>;
}
```
## Props
Props provide a way to pass data from a parent component into a child component to somehow influence their behavior. This is a one way flow, meaning changes to these props only flow from the parent to the child, but not the reverse.
```tsx
// Parent
function Parent() {
return <Child color="red" />;
}
// Child
function Child(props) {
return <h1>{props.color}</h1>;
}
// or
function Child({ color }) {
return <h1>{color}</h1>;
}
```
Props however can also be functions, which is how you would handle passing data from the child back to the parent.
```tsx
function Parent() {
const onSearch = (term) => {
console.log("Searching for", term);
};
return <Child onSubmit={onSearch} />;
}
import { useState } from "react";
function Child({ onSubmit }) {
const [term, setTerm] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(term);
};
return (
<form onSubmit={handleSubmit}>
<input value={term} onChange={(e) => setTerm(e.target.value)} />
</form>
);
}
```
### Children Prop
There is a special prop called `children` which works similarly to slots in Vue. When you use a component and pass in some text or elements within the opening and closing tags, that gets passed as a prop `children` which you can use within your component.
```tsx
function Child({ children }) {
return <h1>{children}</h1>;
}
function Parent() {
return <Child>This is my children!</Child>;
}
```
### With Typescript
When using Typescript, you can define an interface for your props to improve intelisense in your IDE.
```tsx
interface MyProps {
color: string | null; // can be either string or null
shade?: string; // optional prop
}
function Child({ color, shade });
```
## Feeding classNames to child components
To pass classNames to your child components, we can use a package called `classnames` to aggregate multiple classes.
```tsx
import classNames from "classnames";
function Child({ className }) {
const classes = classNames("myclass1 myclass2", className);
return <div className={classes}>My component with custom classes!</div>;
}
```

78
docs/react/context.md Normal file
View File

@@ -0,0 +1,78 @@
# Context
Context is a way of sharing state with many child components without having to pass them via props.
```tsx
// context/foo.ts
import { createContext, useState } from "react";
const MyContext = createContext();
function Provider({ children }) {
const [value, setValue] = useState(0);
const incrementValue = () => {
setValue(value + 1);
};
const valueToShare = {
value,
incrementValue,
};
return (
<MyContext.Provider value={valueToShare}>{children}</MyContext.Provider>
);
}
export { Provider };
export default MyContext;
// In wrapping component...
import { Provider } from "context/foo";
function Parent() {
return (
<Provider>
<Children />
</Provider>
);
}
// In consuming component
import { useContext } from "react";
import { MyContext } from "../context/foo";
function Child() {
const { value, incrementValue } = useContext(MyContext);
}
```
## useCallback
This hook is used to fix some bugs when calling functions stored within contexts from useEffect in a child component. Since useEffect requires the function to be added as a dependency in the second argument, it will rerun that effect whenever that function is called because calling it forces the context to rerender, which in turn creates a new version of that function. So to fix this, we need to use useCallback to create a stable reference to that function.
```tsx
// context/foo.ts
import { createContext, useState, useCallback } from "react";
const MyContext = createContext();
function Provider({ children }) {
const [value, setValue] = useState(0);
const fetchValue = useCallback(() => {
newValue = fetch("localhost:8080");
setValue(newValue);
}, []);
const valueToShare = {
value,
fetchValue,
};
return (
<MyContext.Provider value={valueToShare}>{children}</MyContext.Provider>
);
}
```

38
docs/react/index.md Normal file
View File

@@ -0,0 +1,38 @@
# React
React is a Javascript/Typescript framework for creating responsive single page applications in the web.
## Creating an App
```bash
npm create vite@latest <app-name> -- --template=react-ts
cd <app-name>
npm install
npm run dev
```
### Adding Tailwindcss (Optional)
```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
```js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
```
```css
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```

86
docs/react/jsx.md Normal file
View File

@@ -0,0 +1,86 @@
# JSX
## Rendering a List
```tsx
import { Fragment } from "react";
function Comp() {
const items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" },
];
return (
<div>
{items.map((item) => {
return <Fragment key={item.id}>{item.name}</Fragment>;
})}
</div>
);
}
```
## Conditional Rendering
```tsx
function Comp() {
const items = [
{ id: 1, name: "Item 1", show: true },
{ id: 2, name: "Item 2", show: true },
{ id: 3, name: "Item 3", show: false },
];
return (
<div>
{items.map((item) => {
return {item.show && <Fragment key={item.id}>{item.name}</Fragment>};
})}
</div>
);
}
```
## Checking if item is valid
```tsx
function Comp() {
const items: object[] | null = null;
return <div>{items?.length || "No items"}</div>;
}
```
## Two-way data binding
In order to have two-way data binding between the component and an input or another child component, you need to make use of the `onChange` event or some other event callback.
```tsx
import { useState } from "react";
function Comp() {
const [value, setValue] = useState("");
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
```
## Handling number inputs
```tsx
import { useState } from "react";
function Comp() {
const [value, setValue] = useState(0);
const handleChange = (e) => {
// will be NaN if invalid input or empty
newValue = parseInt(e.target.value) || 0;
// do something with new value
};
// need || "" or else it will always have an annoying 0 in the input
return <input type="number" value={value || ""} onChange={handleChange} />;
}
```

134
docs/react/redux.md Normal file
View File

@@ -0,0 +1,134 @@
# Redux
Redux is a centralized state store that can be accessed from any component in your application. It acts as basically a centralized reducer, where it has a state and an dispatch method to update that state. Redux Toolkit is a library that simplifies creating actions to interact with the store, and is the recommended approach going forward.
```ts
// store/slices/song.ts
import { createSlice } from "@reduxjs/toolkit";
import { reset } from "../actions";
const songsSlice = createSlice({
name: "song",
initialState: [],
reducers: {
addSong(state, action) {
state.push(action.payload);
},
removeSong(state, action) {
// action.payload === string, the song we want to remove
const index = state.indexOf(action.payload);
state.splice(index, 1);
},
},
// listen to manual actions not created by this generator
extraReducers(builder) {
builder.addCase(reset, (state, action) => {
return [];
});
},
});
export const { addSong, removeSong } = songsSlice.actions;
// can also export this as the default
export const songsReducer = songsSlice.reducer;
```
```ts
// store/actions.ts
// Declare common actions shared by multiple reducers
import { createAction } from "@reduxjs/toolkit";
export const reset = createAction("app/reset");
```
```ts
// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { songsReducer, addSong, removeSong } from "./slices/songsSlice";
import { reset } from "./actions";
const store = configureStore({
reducer: {
songs: songsReducer,
},
});
// Export store and slice actions
export { store, reset, addSong, removeSong };
```
```ts
// index.ts
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./store";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<Provider store={store}>
<App />
</Provider>
);
```
```tsx
// components/songs.tsx
import { useDispatch, useSelector } from "react-redux";
import { createRandomSong } from "../data";
import { addSong, removeSong } from "../store";
function SongPlaylist() {
// object to call actions
const dispatch = useDispatch();
// fetch state
// can also filter/map state within here if you want
const songPlaylist = useSelector((state) => {
return state.songs;
});
// dispatch actions with some payload
const handleSongAdd = (song) => {
dispatch(addSong(song));
};
const handleSongRemove = (song) => {
dispatch(removeSong(song));
};
const renderedSongs = songPlaylist.map((song) => {
return (
<li key={song}>
{song}
<button
onClick={() => handleSongRemove(song)}
className="button is-danger"
>
X
</button>
</li>
);
});
return (
<div className="content">
<div className="table-header">
<h3 className="subtitle is-3">Song Playlist</h3>
<div className="buttons">
<button
onClick={() => handleSongAdd(createRandomSong())}
className="button is-link"
>
+ Add Song to Playlist
</button>
</div>
</div>
<ul>{renderedSongs}</ul>
</div>
);
}
export default SongPlaylist;
```

241
docs/react/routing.md Normal file
View File

@@ -0,0 +1,241 @@
# Routing
## Vanilla Routing with Context
```tsx
// context/navigation.ts
import { createContext, useState, useEffect } from "react";
const NavigationContext = createContext();
function NavigationProvider({ children }) {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
// handle front and back arrows
useEffect(() => {
const handler = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener("popstate", handler);
return () => {
window.removeEventListener("popstate", handler);
};
}, []);
const navigate = (to) => {
window.history.pushState({}, "", to);
setCurrentPath(to);
};
return (
<NavigationContext.Provider value={{ currentPath, navigate }}>
{children}
</NavigationContext.Provider>
);
}
export { NavigationProvider };
export default NavigationContext;
```
```tsx
// Route component for conditionally rendering components
import useNavigation from "../hooks/use-navigation";
function Route({ path, children }) {
const { currentPath } = useNavigation();
if (path === currentPath) {
return children;
}
return null;
}
export default Route;
```
```tsx
// example link component
import classNames from "classnames";
import useNavigation from "../hooks/use-navigation";
function Link({ to, children, className, activeClassName }) {
const { navigate, currentPath } = useNavigation();
const classes = classNames(
"text-blue-500",
className,
currentPath === to && activeClassName // apply styling if currently on this page
);
const handleClick = (event) => {
// handle opening new tab, use default behavior
if (event.metaKey || event.ctrlKey) {
return;
}
event.preventDefault();
navigate(to);
};
return (
<a className={classes} href={to} onClick={handleClick}>
{children}
</a>
);
}
export default Link;
```
```tsx
// Navbar component
import Link from "./Link";
function NavBar() {
const links = [
{ label: "Dropdown", path: "/" },
{ label: "Accordion", path: "/accordion" },
{ label: "Buttons", path: "/buttons" },
];
const renderedLinks = links.map((link) => {
return (
<Link
key={link.label}
to={link.path}
className="mb-3"
activeClassName="font-bold border-l-4 border-blue-500 pl-2"
>
{link.label}
</Link>
);
});
return (
<div className="sticky top-0 overflow-y-scroll flex flex-col items-start">
{renderedLinks}
</div>
);
}
export default NavBar;
```
```tsx
// App.ts
import Sidebar from "./components/Sidebar";
import Route from "./components/Route";
import AccordionPage from "./pages/AccordionPage";
import DropdownPage from "./pages/DropdownPage";
import ButtonPage from "./pages/ButtonPage";
function App() {
return (
<div className="container mx-auto grid grid-cols-6 gap-4 mt-4">
<Sidebar />
<div className="col-span-5">
<Route path="/accordion">
<AccordionPage />
</Route>
<Route path="/">
<DropdownPage />
</Route>
<Route path="/buttons">
<ButtonPage />
</Route>
</div>
</div>
);
}
export default App;
```
## Using React Router library
Documentation [here](https://reactrouter.com/home).
```tsx
// main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import HomePage from "./pages/HomePage";
import NotFoundPage from "./pages/NotFoundPage";
import ProfilePage from "./pages/ProfilePage";
import ProfilesPage from "./pages/ProfilesPage";
import "./index.css";
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
errorElement: <NotFoundPage />,
},
{
path: "/profiles",
element: <ProfilesPage />,
children: [
{
path: "/profiles/:profileId",
element: <ProfilePage />,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
```
```tsx
// ProfilesPage.tsx with child component
import { NavLink, Outlet } from "react-router-dom";
export default function ProfilesPage() {
const profiles = [1, 2, 3, 4, 5];
return (
<div className="flex gap-2">
<div className="flex flex-col gap-2">
{profiles.map((profile) => (
// NavLink allows you to apply styling if the link is active
<NavLink
key={profile}
to={`/profiles/${profile}`}
className={({ isActive }) => {
return isActive ? "text-primary-700" : "";
}}
>
Profile {profile}
</NavLink>
))}
</div>
<!-- Outlet displays child component links if active -->
<Outlet />
</div>
);
}
```
```tsx
// ProfilePage component using query params
import { useParams } from "react-router-dom";
export default function ProfilePage() {
const params = useParams<{ profileId: string }>();
return (
<div>
<h1>Profile Page {params.profileId}</h1>
</div>
);
}
```

14
docs/react/sidebar.json Normal file
View File

@@ -0,0 +1,14 @@
[
{
"text": "React Basics",
"items": [
{ "text": "Introduction", "link": "/react/" },
{ "text": "JSX", "link": "/react/jsx" },
{ "text": "Components", "link": "/react/components" },
{ "text": "State", "link": "/react/state" },
{ "text": "Context", "link": "/react/context" },
{ "text": "Routing", "link": "/react/routing" },
{ "text": "Redux", "link": "/react/redux" }
]
}
]

175
docs/react/state.md Normal file
View File

@@ -0,0 +1,175 @@
# State and Events
State allows you to have the user interace with your application, update some data under the hood, and have that reflected in the page.
## Events
Events means how you handle some user actions in your application. A full list of possible events can be found [here](https://react.dev/reference/react-dom/components/common#). Essentially, you define functions that are called when certain actions are performed on elements in your page.
```tsx
function App() {
const handleClick = () => {
console.log("button clicked");
};
return (
<div>
<button onClick={handleClick}>Click me</button>;
<button
onClick={() => {
console.log("Inline function click");
}}
>
Click me too!
</button>
</div>
);
}
```
## useState
State tells your application to rerender the page when something changes in your application.
```tsx
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick} />;
}
```
## useReducer
A Reducer is a way of combining multiple state variables into one object. This allows you to reduce the number of rerenders if multiple pieces of state are changing at the same time. `useReducer` takes a `reducer` function and an initial state. This `reducer` function will run whenever we call `dispatch`, and the argument to `dispatch` will be passed as an argument `action` along with the current state. The `action` argument is usually an object which tells the reducer what type of update to do. Whatever is returned from this function will be the new state. The `reducer` function cannot include async/await, promises, requests, etc. It is only for updating the state. You should not update the state directly unless you are using Immer, which is a library that automatically returns the new state for you.
```tsx
import produce from "immer";
import { useReducer } from "react";
// Use constants for action to prevent typos
const INCREMENT_COUNT = "increment";
const SET_VALUE_TO_ADD = "change_value_to_add";
const DECREMENT_COUNT = "decrement";
const ADD_VALUE_TO_COUNT = "add_value_to_count";
const reducer = (state, action) => {
switch (action.type) {
case INCREMENT_COUNT:
state.count = state.count + 1;
return;
// If not using immer, you would need to do something like...
return {
...state,
count: state.count + 1,
};
case DECREMENT_COUNT:
state.count = state.count - 1;
return;
case ADD_VALUE_TO_COUNT:
state.count = state.count + state.valueToAdd;
state.valueToAdd = 0;
return;
case SET_VALUE_TO_ADD:
state.valueToAdd = action.payload;
return;
default:
return;
}
};
function CounterPage({ initialCount }) {
// wrapper reducer with produce to use immer
const [state, dispatch] = useReducer(produce(reducer), {
count: initialCount,
valueToAdd: 0,
});
const increment = () => {
dispatch({
type: INCREMENT_COUNT,
});
};
const decrement = () => {
dispatch({
type: DECREMENT_COUNT,
});
};
const handleChange = (event) => {
const value = parseInt(event.target.value) || 0;
dispatch({
type: SET_VALUE_TO_ADD,
payload: value,
});
};
const handleSubmit = (event) => {
event.preventDefault();
dispatch({
type: ADD_VALUE_TO_COUNT,
});
};
}
export default CounterPage;
```
### Binding inputs
```tsx
function Input({ onSubmit }) {
const [text, setText] = useState("");
const handleChange = (event) => {
setText(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
onSubmit(text);
setText("");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input className="input" value={title} onChange={handleChange} />
<button className="button">Submit</button>
</form>
</div>
);
}
```
## useEffect
useEffect is a function that runs when the application is initially rendered and sometimes when it is rerendered. The arrow function provided is always called on the initial render, and if a variable passed in the second argument is changed in the rerender, then the arrow function is called again. If no second argument is passed, then the function is called on every rerender.
```tsx
import { useEffect, useState } from "react";
function Comp() {
useEffect(() => {
console.log("Run on initial render");
}, []);
useEffect(() => {
console.log("Run on every render");
});
const [text, setText] = useState("");
useEffect(() => {
console.log("Run when 'text' is changed");
}, [text]);
}
```
The only thing we can return from useEffect is a function, and this function gets called before the next time the useEffect function is run, so from the second render on.