diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 65f6333..930ca57 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -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", diff --git a/docs/frameworks.md b/docs/frameworks.md index 5f8bc08..fc88d73 100644 --- a/docs/frameworks.md +++ b/docs/frameworks.md @@ -1,3 +1,5 @@ # Frameworks [Nuxtjs](/nuxt/) + +[React](/react/) diff --git a/docs/react/components.md b/docs/react/components.md new file mode 100644 index 0000000..9cea729 --- /dev/null +++ b/docs/react/components.md @@ -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

Some component!

; +} +``` + +## 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 +function Child(props) { + return

{props.color}

; +} +// or +function Child({ color }) { + return

{color}

; +} +``` + +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 ; +} + +import { useState } from "react"; + +function Child({ onSubmit }) { + const [term, setTerm] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit(term); + }; + + return ( +
+ setTerm(e.target.value)} /> +
+ ); +} +``` + +### 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

{children}

; +} + +function Parent() { + return This is my children!; +} +``` + +### 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
My component with custom classes!
; +} +``` diff --git a/docs/react/context.md b/docs/react/context.md new file mode 100644 index 0000000..1301679 --- /dev/null +++ b/docs/react/context.md @@ -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 ( + {children} + ); +} + +export { Provider }; +export default MyContext; + +// In wrapping component... +import { Provider } from "context/foo"; + +function Parent() { + return ( + + + + ); +} + +// 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 ( + {children} + ); +} +``` diff --git a/docs/react/index.md b/docs/react/index.md new file mode 100644 index 0000000..ef81e42 --- /dev/null +++ b/docs/react/index.md @@ -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 -- --template=react-ts +cd +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; +``` diff --git a/docs/react/jsx.md b/docs/react/jsx.md new file mode 100644 index 0000000..e39be33 --- /dev/null +++ b/docs/react/jsx.md @@ -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 ( +
+ {items.map((item) => { + return {item.name}; + })} +
+ ); +} +``` + +## 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 ( +
+ {items.map((item) => { + return {item.show && {item.name}}; + })} +
+ ); +} +``` + +## Checking if item is valid + +```tsx +function Comp() { + const items: object[] | null = null; + + return
{items?.length || "No items"}
; +} +``` + +## 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 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 ; +} +``` diff --git a/docs/react/redux.md b/docs/react/redux.md new file mode 100644 index 0000000..833ed25 --- /dev/null +++ b/docs/react/redux.md @@ -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( + + + +); +``` + +```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 ( +
  • + {song} + +
  • + ); + }); + + return ( +
    +
    +

    Song Playlist

    +
    + +
    +
    +
      {renderedSongs}
    +
    + ); +} + +export default SongPlaylist; +``` diff --git a/docs/react/routing.md b/docs/react/routing.md new file mode 100644 index 0000000..2499402 --- /dev/null +++ b/docs/react/routing.md @@ -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 ( + + {children} + + ); +} + +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 ( + + {children} + + ); +} + +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.label} + + ); + }); + + return ( +
    + {renderedLinks} +
    + ); +} + +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 ( +
    + +
    + + + + + + + + + +
    +
    + ); +} + +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: , + errorElement: , + }, + { + path: "/profiles", + element: , + children: [ + { + path: "/profiles/:profileId", + element: , + }, + ], + }, +]); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); +``` + +```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 ( +
    +
    + {profiles.map((profile) => ( + // NavLink allows you to apply styling if the link is active + { + return isActive ? "text-primary-700" : ""; + }} + > + Profile {profile} + + ))} +
    + + +
    + ); +} +``` + +```tsx +// ProfilePage component using query params +import { useParams } from "react-router-dom"; + +export default function ProfilePage() { + const params = useParams<{ profileId: string }>(); + return ( +
    +

    Profile Page {params.profileId}

    +
    + ); +} +``` diff --git a/docs/react/sidebar.json b/docs/react/sidebar.json new file mode 100644 index 0000000..df2bf08 --- /dev/null +++ b/docs/react/sidebar.json @@ -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" } + ] + } +] diff --git a/docs/react/state.md b/docs/react/state.md new file mode 100644 index 0000000..e1411da --- /dev/null +++ b/docs/react/state.md @@ -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 ( +
    + ; + +
    + ); +} +``` + +## 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 + + + ); +} +``` + +## 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.