Skip to content

pnlinh-it/hello-reactjs

Repository files navigation

Image

image

Setup

yarn config set ignore-engines true 
yarn create react-app hello-react --template=typescript
yarn eject
yarn upgrade -R eslint 
# Prettier
yarn add [email protected] [email protected] [email protected]
# Redux toolkit
yarn add @reduxjs/[email protected] @types/[email protected] [email protected] [email protected]
yarn create react-app my-app --template redux-typescript
  • Add .prettierrc, .eslintrc.json, .eslintignore

Eslint is already added:

{
  "@typescript-eslint/eslint-plugin": "^4.30.0",
  "@typescript-eslint/parser": "^4.30.0",
  "babel-eslint": "^10.1.0",
  "eslint": "^7.32.0",
  "eslint-config-react-app": "^6.0.0",
  "eslint-plugin-flowtype": "^5.2.0",
  "eslint-plugin-import": "^2.22.1",
  "eslint-plugin-jest": "^24.1.0",
  "eslint-plugin-jsx-a11y": "^6.3.1",
  "eslint-plugin-react": "^7.21.5",
  "eslint-plugin-react-hooks": "^4.2.0",
  "eslint-plugin-testing-library": "^3.9.2",
  "eslint-webpack-plugin": "^2.5.2",
  "eslint-plugin-prettier": "4.0.0",
  "eslint-config-prettier": "8.3.0",
  "prettier": "2.3.2"
}

Find eslint-plugin-prettier properly version

  • Get currently eslint version: yarn list eslint
❯ yarn info eslint version
yarn list v1.22.4
warning Filtering by arguments is deprecated. Please use the pattern option instead.
└─ [email protected]
✨  Done in 0.73s.

Eslint and Prettier

  • Eslint is linter prettier is formatter
  • eslint-config-prettier : Turn off Eslint rules that is conflict with Prettier
  • eslint-plugin-prettier : Integrate Prettier rules into Eslint rules
{
  "extends": [
    "prettier"
  ],
  "plugins": [
    "prettier"
  ],
  "rules": {
    "prettier/prettier": [
      "error"
    ]
  }
}

Props

Values of props

// JSX:
<MyComponent prop={<Message who="Joker" />} />

// Variables having any kind of value:
<MyComponent prop={myVariable} />

// String literals:
<MyComponent prop="My String Value" />

// Template literals with variables:
<MyComponent prop={`My String Value ${myVariable}`} />

// Number literals:
<MyComponent prop={42} />

// Boolean literals:
<MyComponent prop={false} />

// Plain object literals:
<MyComponent prop={{ property: 'Value' }} />

// Array literals:
<MyComponent prop={['Item 1', 'Item 2']} />
function HelloOptional({ who = 'Unknown' }) {
  return <div>Hello, {who}!</div>;
}
const hiBatman = { greet: 'Hi', who: 'Batman' };
function Message({ greet, who }) {
  return <div>{greet}, {who}!</div>;
}

<Message greet={hiBatman.greet} who={hiBatman.who} />

<Message {...hiBatman} />

Children prop

type Props = {
  children: React.ReactNode;
};

function Parent({ children }: Props) {
  console.log(children); // logs <span>I'm a child!</span>
  return <div>{children}</div>;
}

<Parent>
  <span>I'm a child!</span>
</Parent>

Hook

useMemo

  • const memoizedResult = useMemo(compute, dependencies);
const memoizedResult = useMemo(() => {
  return expensiveFunction(propA, propB);
}, [propA, propB]);
export function CalculateFactorial() {
  const [number, setNumber] = useState(1);
  // const factorial = factorialOf(number);
  const factorial = useMemo(() => factorialOf(number), [number]);
  const onClick = () => setNumber(randomNumber);
  
  return (
    <div>
      <button onClick={onClick}>Re-render</button>
    </div>
  );
}

useContext

 const UserContext = createContext({
  userName: '',
  setUserName: () => {},
});
function Application() {
  const [userName, setUserName] = useState('John Smith');
  // Pass update sate function to context value
  const value = useMemo(
    () => ({ userName, setUserName }),
    [userName]
  );

  return (
    <UserContext.Provider value={value}>
      <UserNameInput />
      <UserInfo />
    </UserContext.Provider>
  );
}
function UserNameInput() {
  const { userName, setUserName } = useContext(UserContext);
  const changeHandler = event => setUserName(event.target.value);
  return (
    <input
      type="text"
      value={userName}
      onChange={changeHandler}
    />
  );
}
function UserInfo() {
  const { userName } = useContext(UserContext);
  return <span>{userName}</span>;
}

useEffect

useEffect(() => {
  // This will execute EVERY every rendering
})

// Typically, use to fetch data
// https://dmitripavlutin.com/react-useeffect-explanation/#61-fetching-data
useEffect(() => {
  // This will execute ONCE after initial rendering (mounted)
}, [])

const [state, setState] = useState('');
useEffect(() => {
  // Runs ONCE after initial rendering
  // and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state])

useEffect(() => {
  // Side-effect...
  return function cleanup() {
    // Side-effect cleanup...
  };
}, dependencies);
// Mounting: Run Effect
// Dependencies change: Cleanup → Run Effect
// Dependencies change: Cleanup → Run Effect
// Unmonting: Cleanup
// A) After initial rendering, useEffect() invokes the callback having the side-effect. cleanup function is not invoked.
// B) On later renderings, before invoking the next side-effect callback, useEffect() invokes the cleanup function from the previous side-effect execution (to clean up everything after the previous side-effect), then runs the current side-effect.
// C) Finally, after unmounting the component, useEffect() invokes the cleanup function from the latest side-effect.
useEffect(() => {
  const id = setInterval(() => { console.log(message); }, 2000);
  return () => { clearInterval(id); };
}, [message]);
useEffect(() => {
  // Infinite loop!
  setState(count + 1);
});
useEffect(() => {
  // No infinite loop
  setState(count + 1);
}, [whenToUpdateValue]);

useEffect(() => {
  // Infinite loop!
  setObject({
    ...object,
    prop: 'newValue'
  })
}, [object]);
useEffect(() => {
  // No infinite loop
  setObject({
    ...object,
    prop: 'newValue'
  })
}, [object.whenToUpdateProp]);
  • Avoid pass async function as first parameter. Create async function and invoke it instead.
useEffect(() => {
  async function fetchMyAPI() {
    let response = await fetch('api/data')
    response = await response.json()
    dataSet(response)
  }

  fetchMyAPI()
}, [])

async function fetchData() {
  const res = await fetch("https://swapi.co/api/planets/4/");
  res.json()
    .then(res => setPosts(res)) // update state
    .catch(err => setErrors(err)); // update state
}

useEffect(() => {
  fetchData();
}, []);

useRef

const domRef = useRef<HTMLInputElement>(null);
const handleDomNodeChange = (domNode: HTMLInputElement | null) => {
  console.log(`Dom ref change: ${domNode}`);
};

return (
        <div>
          <input name="email" ref={domRef}/>
          <input name="name" ref={handleDomNodeChange}/>
        </div>
);

// Ref is null on initial rendering: https://dmitripavlutin.com/react-useref-guide/#ref-is-null-on-initial-rendering
const inputRef = useRef();
useEffect(() => {
  // Logs `HTMLInputElement` 
  console.log(inputRef.current);
  inputRef.current.focus();
}, []);
// Logs `undefined` during initial rendering
console.log(inputRef.current);
return <input ref={inputRef} type="text" />;

Practice

We can store element in a variable: bulb = <div className={on ? 'bulb-on' : 'bulb-off'}/>;. See Element Variables

function Bulbs() {
  const [on, setOn] = useState(false);
  const [count, setCount] = useState(1);
  const lightSwitch = () => setOn(on => !on);
  const addBulbs = () => setCount(count => count + 1);
  // Element variable
  const bulb = <div className={on ? 'bulb-on' : 'bulb-off'}/>;
  const bulbs = Array(count).fill(bulb);
  return (
          <>
            <div className="bulbs">{bulbs}</div>
            <button onClick={lightSwitch}>On/off</button>
            <button onClick={addBulbs}>Add bulb</button>
          </>
  );
}

const COLORS = ['white', 'red', 'blue', 'black', 'cream'];
function RegisterYourCatForm() {
  return (
    <select>
      <option value="">Select color</option>
      {COLORS.map(c => <option key={c}>{c}</option>)}
    </select>
  );
}
interface Props {
    bigJsonData: string;
}
function MyComponent({ bigJsonData }: Props) {
  const [value, setValue] = useState(function getInitialState() {
    const object = JSON.parse(bigJsonData); // expensive operation
    return object.initialValue;
  });
}

The problem with the current implementation is that depends on how data is fetched.

function EmployeesPage() {
  const [isFetching, setFetching] = useState(false);
  const [employees, setEmployees] = useState([]);
  
  // This should be put in another place
  useEffect(function fetch() {
    (async function() {
      setFetching(true);
      const response = await axios.get("/employees");
      setEmployees(response.data);
      setFetching(false);
    })();
  }, []);
  
  if (isFetching) {
    return <div>Fetching employees....</div>;
  }
  
  return <EmployeesList employees={employees} />;
}
function EmployeesPage({resource}) {
  return (
    <Suspense fallback={<h1>Fetching employees....</h1>}>
      <EmployeesFetch resource={resource}/>
    </Suspense>
  );
}

function EmployeesFetch({resource}) {
  const employees = resource.employees.read();
  return <EmployeesList employees={employees}/>;
}

Implement scroll to top with custom hook

import React, { useState, useEffect } from 'react';
const DISTANCE = 500;
function ScrollToTop() {
  const [crossed, setCrossed] = useState(false);
  useEffect(
    function() {
      const handler = () => setCrossed(window.scrollY > DISTANCE);
      handler();
      window.addEventListener("scroll", handler);
      return () => window.removeEventListener("scroll", handler);
    },
    []
  );
  function onClick() {
    window.scrollTo({
      top: 0,
      behavior: "smooth"
    });
  }
  if (!crossed) {
    return null;
  }
  return <button onClick={onClick}>Jump to top</button>;
}
// Custom hook
function useScrollDistance(distance) {
  // Invoke component re-render will crossed change
  const [crossed, setCrossed] = useState(false);
  
  // We use useEffect to register window.addEventListener
  useEffect(function() {
    const handler = () => setCrossed(window.scrollY > distance);
    handler();
    window.addEventListener("scroll", handler);
    return () => window.removeEventListener("scroll", handler);
  }, [distance]);
  
  return crossed;
}

function IfScrollCrossed({ children, distance }) {
  const isBottom = useScrollDistance(distance);
  return isBottom ? children : null;
}

function onClick() {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  });
}
function JumpToTop() {
  return <button onClick={onClick}>Jump to top</button>;
}

const DISTANCE = 500;
function MyComponent() {
  return (
    <IfScrollCrossed distance={DISTANCE}>
      <JumpToTop />
    </IfScrollCrossed>
  );
}
function ProductsList() {
  const [names, setNames] = useState([]);
  const handleAdd = () => {
    const s = new Set([...names, newName]);
    setNames([...s]);
  };
  return (
    <div className="products">
      <button onClick={handleAdd}>Add</button>
    </div>
  );
}

// Custom hook
export function useUnique(initial) {
  const [items, setItems] = useState(initial);
  // invoke add(newItem) will trigger component re-render 
  const add = newItem => {
    const uniqueItems = [...new Set([...items, newItem])];
    setItems(uniqueItems);
  };
  return [items, add];
};

function ProductsList() {
  const [names, add] = useUnique([]);
  // add(newName) will trigger component re-render
  const handleAdd = () => add(newName);
  return (
    <div className="products">
      <button onClick={handleAdd}>Add</button>
    </div>
  );
}

Mistake

Invoking hooks must be the same order between renderings — always!

function FetchGame({id}) {
  // Don't do this
  // if (!id) {
  //   return 'Please select a game to fetch';
  // }
  const [game, setGame] = useState({
    name: '',
    description: ''
  });
  useEffect(() => {
    const fetchGame = async () => {
      const response = await fetch(`/api/game/${id}`);
      const fetchedGame = await response.json();
      setGame(fetchedGame);
    };
    if (id) {
      fetchGame();
    }
  }, [id]);
  if (!id) {
    return 'Please select a game to fetch';
  }
  return (
          <div>
            <div>Name: {game.name}</div>
            <div>Description: {game.description}</div>
          </div>
  );
}

Using setCount(count => count + 1); instead of setCount(count + 1);.

Other example: https://dmitripavlutin.com/react-usestate-hook-guide/#42-stale-state

setInterval still invokes the old log closure that has captured count as 0 from the initial rendering.

log is a stale closure because it has captured a stale (in other words outdated) state variable count.

// Don't do this.
const [count, setCount] = useState(0);
useEffect(function () {
  setInterval(function log() {
    console.log(`Count is: ${count}`);
  }, 2000);
}, []);

Using a useEffect cleaner function.

useEffect(function () {
  const id = setInterval(function log() {
    console.log(`Count is: ${count}`);
  }, 2000);
  return () => clearInterval(id);
}, [count]);

Reference

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published