Back

TSX: TypeScript + React

Create project

npm create vite@latest project-name
npm i ts-node-dev -D
//package.json
{
  "scripts": {
    "dev": "ts-node-dev --respawn src/index.ts"
  }
}

Add linter

# initialize the linter with
npx eslint --init
# or
npm init @eslint/config@latest
// eslint.config.js

export default [
  {
    files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
    languageOptions: {
      globals: globals.browser,
      parser: tseslint.parser,
      parserOptions: {
        ecmaVersion: 2021,
        sourceType: "module",
        ecmaFeatures: {
          jsx: true
        }
      }
    },
    plugins: ["react"],
    settings: {
      react: {
        version: "detect"
      }
    },
    rules: {
      "react/react-in-jsx-scope": "off",
      "react/jsx-uses-react": "off"
    }
  },
  ...tseslint.configs.recommended,
  pluginReact.configs.recommended
];

React.FC

const App: React.FC = () => {
  // ✔️
  return <h1>Todo List App</h1>;
};
const App = () => {
  // ✔️
  return <h1>Todo List App</h1>;
};

Adding Types to Props

To enable type checking for the props, we first need to declare types for them.

const App = ({ message }: { message: string }) => {
  return <div>{message}</div>;
};
type AppProps = {
  message: string;
};

// If exporting, use `interface` so that consumers can extend it.
interface AppProps {
  message: string;
}
const App = ({ message }: AppProps): React.JSX.Element => {
  return <div>{message}</div>;
};

JSX.Element is the result of rendering a React.FC, not a component in itself.

const App = ({ message }: AppProps) => {
  return <div>{message}</div>;
};
const App: React.FC<AppProps> = ({ message }) => {
  return <div>{message}</div>;
};

useState

function App() {
  const [num, setNum] = useState(5); // number inferred

  const changeNumber = () => {
    setNum("2"); // ❌
  };

  return (
    <div className="App">
      {num}
      <button onClick={changeNumber}>Change number</button>
    </div>
  );
}

export default App;
function App() {
  const [num, setNum] = useState<number | string>(5);

  const changeNumber = () => {
    setNum("2"); // ✔️
  };

  return (
    <div className="App">
      {num}
      <button onClick={changeNumber}>Change number</button>
    </div>
  );
}

export default App;

List of Components

function App() {
  const [subs, setSubs] = useState([]); // ❌

  return (
    <div className="App">
      <h1>subs</h1>
      <ul>
        {subs.map((sub) => {
          return (
            <li key={sub.nick}>
              <img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
              <h4>
                {sub.nick} <small>{sub.subMonths}</small>
              </h4>
              <p>{sub.description?.substring(0, 100)}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
interface Sub {
  nick: string;
  avatar: string;
  subMonths: number;
  description?: string;
}

function App() {
  const [subs, setSubs] = useState<Array<Sub>>([]); // ✔️

  return (
    <div className="App">
      <h1>subs</h1>
      <ul>
        {subs.map((sub) => {
          return (
            <li key={sub.nick}>
              <img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
              <h4>
                {sub.nick} <small>{sub.subMonths}</small>
              </h4>
              <p>{sub.description?.substring(0, 100)}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

Abstracting

interface Sub {
  nick: string;
  avatar: string;
  subMonths: number;
  description?: string;
}

interface AppState {
  subs: Array<Sub>;
}

function App() {
  const [subs, setSubs] = useState<AppState["subs"]>([]); // ✔️

  return (
    <div className="App">
      <h1>subs</h1>
      <ul>
        {subs.map((sub) => {
          return (
            <li key={sub.nick}>
              <img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
              <h4>
                {sub.nick} <small>{sub.subMonths}</small>
              </h4>
              <p>{sub.description?.substring(0, 100)}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
// List.tsx

interface Sub {
  nick: string;
  avatar: string;
  subMonths: number;
  description?: string;
}

interface Props {
  subs: Array<Sub>;
}

export default function List({ subs }: Props) {
  // ✔️
  return (
    <ul>
      {subs.map((sub) => {
        return (
          <li key={sub.nick}>
            <img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
            <h4>
              {sub.nick} <small>{sub.subMonths}</small>
            </h4>
            <p>{sub.description?.substring(0, 100)}</p>
          </li>
        );
      })}
    </ul>
  );
}
// App.tsx

import List from "./List";

interface Sub {
  nick: string;
  avatar: string;
  subMonths: number;
  description?: string;
}

interface AppState {
  subs: Array<Sub>;
}

function App() {
  const [subs, setSubs] = useState<AppState["subs"]>([]); // ✔️

  return (
    <div className="App">
      <h1>subs</h1>
      <List subs={subs} />
    </div>
  );
}
// types.d.ts
export interface Sub {
  nick: string;
  subMonths: number;
  avatar: string;
  description: string;
}

Forms

import { Sub } from "../types";

interface FormState {
  inputValues: Sub;
}

const Form = () => {
  const [inputValues, setInputValues] = useState<FormState["inputValues"]>({
    nick: "",
    subMonths: 0,
    avatar: "",
    description: ""
  });
  // form state setup
};
// form change handler
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
  setInputValues({
    ...inputValues,
    [evt.target.name]: evt.target.value
  });
};

return (
  <div>
    <form onSubmit={handleSubmit}>
      <input
        onChange={handleChange}
        value={inputValues.nick}
        type="text"
        name="nick"
        placeholder="nick"
      />
      <button>Save new sub!</button>
    </form>
  </div>
);
// App.tsx

function App() {
  const [subs, setSubs] = useState<AppState["subs"]>([]);
  const [newSubsNumber, setNewSubsNumber] =
    useState<AppState["newSubsNumber"]>(0);

  useEffect(() => {
    setSubs(INITIAL_STATE);
  }, []);

  return (
    <div className="App">
      <h1>midu subs</h1>
      <List subs={subs} />
      <Form onNewSub={setSubs} />
    </div>
  );
}

export default App;
// Form.tsx

interface FormProps {
  onNewSub: React.Dispatch<React.SetStateAction<Sub[]>>;
}

const Form = ({ onNewSub }: FormProps) => {
  const [inputValues, setInputValues] = useState<FormState["inputValues"]>({
    nick: "",
    subMonths: 0,
    avatar: "",
    description: ""
  });
};

const handleSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
  evt.preventDefault();
  onNewSub((subs) => [...subs, inputValues]);
};
// App.tsx
function App() {
  const [subs, setSubs] = useState<AppState["subs"]>([]);
  const [newSubsNumber, setNewSubsNumber] =
    useState<AppState["newSubsNumber"]>(0);

  useEffect(() => {
    setSubs(INITIAL_STATE);
  }, []);

  const handleNewSub = (newSub: Sub): void => {
    setSubs((subs) => [...subs, newSub]);
  };

  return (
    <div className="App">
      <h1>midu subs</h1>
      <List subs={subs} />
      <Form onNewSub={handleNewSub} />
    </div>
  );
}

export default App;
interface FormState {
  inputValues: Sub;
}

interface FormProps {
  onNewSub: (newSub: Sub) => void;
}

const Form = ({ onNewSub }: FormProps) => {
  const [inputValues, setInputValues] = useState<FormState["inputValues"]>({
    nick: "",
    subMonths: 0,
    avatar: "",
    description: ""
  });

  const handleSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
    onNewSub(inputValues);
  };
};

useRef

import { useRef } from "react";

const divRef = useRef<HTMLDivElement>(null);

<div className="App" ref={divRef}></div>;

useReducer

type FormReducerAction =
  | {
      type: "change_value";
      payload: {
        inputName: string;
        inputValue: string;
      };
    }
  | {
      type: "clear";
    };

const formReducer = (
  state: FormState["inputValues"],
  action: FormReducerAction
) => {
  switch (action.type) {
    case "change_value": {
      const { inputName, inputValue } = action.payload;
      return {
        ...state,
        [inputName]: inputValue
      };
    }
    case "clear":
      return INITIAL_STATE;
    default:
      return state;
  }
};

const Form = ({ onNewSub }: FormProps) => {
  const [inputValues, dispatch] = useReducer(formReducer, INITIAL_STATE);

  const handleSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
    onNewSub(inputValues);
    handleClear();
  };

  const handleChange = (
    evt: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = evt.target;

    dispatch({
      type: "change_value",
      payload: {
        inputName: name,
        inputValue: value
      }
    });
  };

  const handleClear = () => {
    dispatch({ type: "clear" });
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          onChange={handleChange}
          value={inputValues.name}
          type="text"
          name="name"
        />
        <button onClick={handleSubmit} type="submit">
          Clear
        </button>
      </form>
    </div>
  );
};

Fetching

useEffect(() => {
  const fetchSubs = (): Promise<SubsResponseFromApi> => {
    return fetch("http://localhost:3001/subs").then((res) => res.json());
  };

  fetchSubs().then(setSubs);
}, []);
useEffect(() => {
  const fetchSubs = () => {
    return axios
      .get<SubsResponseFromApi>("http://localhost:3001/subs")
      .then((response) => response.data);
  };

  fetchSubs().then(setSubs);
}, []);

Resources

GitHub - typescript-cheatsheets/react: Cheatsheets for experienced React developers getting started with TypeScript

TypeScript + React notes