Główne logo strony

Shorts

Krótkie informacje / sztuczki / rozwiązania, które mogą Ci się przydać w codziennej pracy 😉

Masz pomysł na nietypowego i pomocnego Shorta? Daj mi o tym znać na Discordzie🔗

Edycja danych z API w DevTools

W Chrome DevTools możemy edytować zapytania fetch i XHR 😲

Jest to dość przydatne do szybkiego mockowania API bez faktycznych zmian na BE czy budowania mock servera.

Począwszy od Chrome 117, DevTools może zastąpić zarówno dane w responsie jak i jego nagłówki.

Sprawdź również Demo oraz dokumentację.

Stylowanie zagnieżdżonych elementów w Tailwind CSS

Czy wiesz, że w Tailwind CSS możesz stylować zagnieżdżone elementy w sposób bardziej zbliżony do tradycyjnego CSS? To możliwe dzięki tzw. arbitrary variants. Pozwalają one na tworzenie niestandardowych selektorów.

Rozważmy przykład, w którym chcemy obrócić ikonę SVG w zależności od stanu :open elementu details. W tradycyjnym CSS, użylibyśmy selektora details:open svg { rotate: 0.5turn; }. W Tailwind możemy osiągnąć podobny efekt za pomocą arbitrary variants.

Oto jak to zrobimy w Tailwind:

<details class="[&_svg]:open:-rotate-180">
  <summary>
    Otwórz mnie
    <svg>
      <!-- SVG stuff -->
    </svg>
  </summary>
</details>

Ta składnia Tailwind, [&_svg]:open:-rotate-180, jest interpretowana następująco:

Dzięki temu możemy tworzyć złożone i zaawansowane selektory w sposób zbliżony do klasycznego CSS, jednocześnie korzystając z tego, co daje nam Tailwind CSS.

Zobacz również ten short na YT z innym przykładem.

React Context - rerenderuj tylko to, co potrzebne

Gdy używamy API Context w React, często popełniamy błąd polegający na tworzeniu kontekstu i dostarczaniu go bezpośrednio w głównym komponencie aplikacji (App). W takim przypadku, każda zmiana stanu w kontekście powoduje ponowne renderowanie wszystkich komponentów, nawet tych, które nie używają tego kontekstu.

Przykład błędnego użycia

const CountContext = React.createContext();
 
function App() {
  const [count, setCount] = React.useState(0);
 
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <ComponentOne />
      <ComponentTwo />
    </CountContext.Provider>
  );
}

Jak to zrobić lepiej?

Aby uniknąć niepotrzebnego rerenderowania, należy przenieść logikę stanu i kontekstu do osobnego komponentu typu Provider:

// CountContextProvider.jsx
export const CountContext = React.createContext();
 
export const CountContextProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
 
  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
};

Następnie używamy CountContextProvider w głównym komponencie aplikacji, otaczając tylko te komponenty, które faktycznie potrzebują dostępu do tego kontekstu.

// App.jsx
import { CountContextProvider } from "./CountContextProvider";
 
function App() {
  return (
    <CountContextProvider>
      <ComponentOne />
      <ComponentTwo />
    </CountContextProvider>
  );
}

Dzięki temu tylko te komponenty, które używają kontekstu (za pomocą useContext), będą ponownie renderowane przy zmianie stanu, co znacznie poprawia wydajność aplikacji.

Zobacz więc tutaj.

Nowy składnik npm query do selekcji zależności

Czy wiesz, że dzięki komendzie npm query oraz selektorom zależności, opartych na znanej składni CSS możesz szybkio i elastycznie filtrować zależności w projekcie Node.js?

Oto kilka przykładów:

  1. Wypisanie wszystkich zależności (podobnie jak npm list --all):

    npm query "*"
  2. Znalezienie wszystkich wersji react i lodash w projekcie:

    npm query "#react, #lodash"
  3. Wyszukiwanie wersji react, które nie są zależnościami typu peer:

    npm query "#react:not(.peer)"
  4. Znalezienie zależności z licencją MIT:

    npm query "[license=MIT]"
  5. Wyszukiwanie zależności z repozytorium git:

    npm query ":type(git)"
  6. Sprawdzenie, które zależności transakcyjne używają skryptu postinstall:

    npm query ":attr(scripts, [postinstall]):not(:root > *)"

👉 Więcej informacji do znalezienia tutaj, tutaj oraz tutaj.

Odnoszenie się do wartości z package.json w skryptach npm

Czy wiesz, że możesz odnosić się do wartości zdefiniowanych w pliku package.json bezpośrednio w definicjach skryptów npm/yarn? To świetne rozwiązanie, które pomaga uniknąć powtórzeń, szczególnie w większych projektach.

Przyjrzyjmy się przykładowi:

{
  "name": "my-package",
  "config": {
    "src": "./src/*"
  },
  "scripts": {
    "lint": "eslint $npm_package_config_src",
    "test": "jest $npm_package_config_src"
  }
}

W powyższym przykładzie zdefiniowaliśmy ścieżkę ./src/* jako część konfiguracji i odnosimy się do niej w skryptach lint i test. Dzięki temu, jeśli ścieżka ulegnie zmianie, wystarczy zaktualizować ją tylko w jednym miejscu.

Dodatkowo, wartości z package.json są dostępne również w Node.js poprzez process.env, gdy uruchamiasz skrypty za pomocą yarn:

// package.json
{
  "foo": "bar",
  "scripts": {
    "start": "node index.js"
  }
}
// index.js
console.log(process.env.npm_package_foo); // 'bar'

Kiedy uruchomisz yarn start, plik index.js będzie miał dostęp do wartości z package.json poprzez process.env.

To super pomocne rozwiązanie, które może znacznie upraszczać zarządzanie skryptami i konfiguracją w projektach!

Mierzenie wydajności w JavaScript z performance.now()

Czy wiesz, że w JavaScript możemy precyzyjnie mierzyć czas wykonania naszego kodu za pomocą funkcji performance.now()? Jest ona znacznie dokładniejsza niż Date.now() i pozwala na uzyskanie wyniku z dokładnością do mikrosekund.

Spójrzmy na przykład:

const start = performance.now();
 
// Tutaj umieść swój kod, którego wydajność chcesz zmierzyć
for (let i = 0; i < 1000; i++) {
  console.log(i);
}
 
const end = performance.now();
console.log(`Czas wykonania: ${end - start} milisekund`);

W powyższym przykładzie mierzymy czas wykonania pętli for. Funkcja performance.now() zwraca czas w milisekundach od momentu uruchomienia strony / aplikacji, więc poprzez odjęcie czasu startu od czasu końca, otrzymujemy czas wykonania naszego kodu.

Jest to szczególnie użyteczne podczas optymalizacji wydajności aplikacji, gdy chcemy zrozumieć, które fragmenty kodu są "wąskimi gardłami" naszej aplikacji. Albo gdy chcemy udowodnić, że nasz kod jest szybszy od kodu napisanego przez kolegę 😎.

Kondycyjne klasy w Tailwind CSS

Kondycyjne dodawanie klas w Tailwindzie nie jest tak oczywiste, jak mogłoby się wydawać. Tailwind generuje pliki CSS na podstawie klas które znajdzie w kodzie, więc dodawanie logiki warunkowej do klas może spowodować, że dana klasa nie zostanie uwzględniona przez Tailwinda w trakcie budowania strony. Na szczęście możemy to łatwo rozwiązać za pomocą małego helpera:

import clsx, { type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
 
/** Utility function to add Tailwind classes conditionally */
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Przykład użycia w komponencie Reacta:

import { cn } from "./utils/cn";
 
export function Button({ variant = "primary" }) {
  return (
    <button
      className={cn("px-4 py-2 rounded-md", {
        "bg-blue-500 text-white": variant === "primary", // klasy dodane tylko wtedy, gdy "variant" jest równy "primary"
        "bg-gray-200 text-gray-800": variant === "secondary", // klasy dodane tylko wtedy, gdy "variant" jest równy "secondary"
      })}
    >
      Click me
    </button>
  );
}

👉 Więcej informacji o tym helperze możecie znaleźć tutaj oraz tutaj.

document.designMode

Tryb projektowania jest tą funkcjonalnością w przeglądarce, która pozwala nam bezpośrednio edytować dokument (czyli zawartość tekstową strony internetowej). Domyślnie ta właściwość jest oczywiście wyłączona, ale aby uruchomić ten feature, wystarczy otworzyć DevToolsy a następnie wpisać w konsoli:

document.designMode="on"

Oczywiście możemy osiągnąć to samo edytując HTML bezpośrednio w DevToolsach, ale trzeba przyznać, że jest to o wiele wygodniejsze rozwiązanie 😉.

Przykładowe użycie 👉 https://twitter.com/sulco/status/1177559150563344384

Usuwanie nieużywanych node_modules

Wszyscy znamy problem gromadzenia się wielu repozytoriów na naszym dysku. Przy projektach korzystających z Node.js, foldery node_modules mogą zajmować setki megabajtów 🤯. Ręczne ich usuwanie to dośc żmudna praca, ale na szczęście istnieje prosty sposób - paczka npkill.

Możemy ją uruchomić bezpośrednio w terminalu:

$ npx npkill

Po uruchomieniu tego polecenia, npkill wyświetli listę wszystkich folderów node_modules w danym, oraz w zagnieżdżonych katalogach i pozwoli na ich błyskawiczne usunięcie z dysku 🧹.

Typowanie process.env w TypeScript

Szybki, czysty i prosty sposób na typowanie process.env w TypeScript z użyciem biblioteki zod.

import { z } from "zod";
 
const envVariables = z.object({
  DATABASE_URL: z.string(),
  CUSTOM_STUFF: z.string(),
});
 
envVariables.parse(process.env);
 
declare global {
  namespace NodeJS {
    interface ProcessEnv extends z.infer<typeof envVariables> {}
  }
}
 
process.env.DATABASE_URL;
//          ^ Auto-completion 🥳

Zobacz więcej tutaj.

Loose Autocomplete w TypeScript

Chcesz, aby zdefiniowany przez Ciebie typ union przyjmował również ogólnego string-a, ale zachował możliwość automatycznego uzupełniania zdefiniowanych wartości? Możesz użyć pewnego tricku, aby to osiągnąć 🪄

Oto przykład:

// VS Code nie będzie podpowiadał wartości "foo", "bar" i "baz" 😢
type MyType = "foo" | "bar" | "baz" | string;
 
// Możemy podać dowolny string, ale VS Code będzie podpowiadał wartości "foo", "bar" i "baz" 🤩
type MyBetterType = "foo" | "bar" | "baz" | (string & {});

Teraz nasz typ jest bardziej elastyczny, ale nadal możemy korzystać z automatycznego uzupełniania w VS Code 💪

useCallback i debounce w React

Szybki snippet na to, aby połączyć ze sobą useCallback oraz funkcję debounce() (chyba nie muszę nikomu jej przedstawiać 😉) z biblioteki lodash (albo z naszej własnej implementacji).

import { useMemo } from "react";
import debounce from "lodash.debounce";
 
function MyComponent() {
  const changeHandler = () => {
    // handle the event...
  };
  const eventHandler = () => {
    // handle the event...
  };
 
  // Option A: useCallback() stores the debounced callback
  const debouncedChangeHandler = useCallback(debounce(changeHandler, 300), []);
 
  // Option B: useMemo() stores the debounced callback
  const debouncedEventHandler = useMemo(() => debounce(eventHandler, 300), []);
 
  // ...
}

Sprawdź dokładniejszy opis i więcej przykładów w tym artykule.

extends keyof vs. in keyof w TypeScript

Czy zdarzyło Ci się czasami trochę pogubić lub nie do końca rozumieć różnicę pomiędzy extends keyof i in keyof w TypeScript?

Oto krótkie wyjaśnienie:

extends keyof jest używane w TypeScript dla typów generycznych. Wskazuje, że typ generyczny musi być kluczem określonego obiektu.

Oto prosty przykład:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
console.log(getProperty(x, "a")); // Output: 1
console.log(getProperty(x, "m")); // Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

W tym przykładzie K extends keyof T oznacza, że K powinien być kluczem obiektu T. Dlatego TypeScript wyświetli błąd, jeśli spróbujesz użyć właściwości, która nie istnieje w obiekcie.

Wyrażenie in keyof jest używane z typami mapowanymi, które są sposobem na tworzenie nowych typów na podstawie starych. Tak więc in keyof pozwala nam iterować po kluczach typu obiektu.

Brzmi może nieco trudniej, ale spójrzmy na kolejny przykład:

interface User {
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  joinedDate: Date;
}
 
type AllStrings<T> = {
  [K in keyof T]: string; // nasz nowy typ będzie zawierał wszystkie klucze z T
};
 
let user: User = {
  firstName: "John",
  lastName: "Doe",
  email: "john.doe@example.com",
  age: 30,
  joinedDate: new Date(2023, 1, 1),
};
 
let userInStrings: AllStrings<User> = {
  firstName: user.firstName,
  lastName: user.lastName,
  email: user.email,
  age: String(user.age),
  joinedDate: user.joinedDate.toISOString(),
};
 
console.log(userInStrings);
// Output: { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', age: '30', joinedDate: '2023-02-01T00:00:00.000Z' }

Tutaj AllStrings<T> jest typem mapowanym, który konwertuje wszystkie właściwości T na ciągi znaków. Używamy in keyof ([K in keyof T]), aby przejść przez każdy klucz w T i przypisać mu nowy typ (w tym przypadku string). W userInStrings używamy tego zmapowanego typu do utworzenia obiektu, w którym wszystkie wartości są łańcuchami, co może być przydatne do niektórych rodzajów przetwarzania, serializacji, rejestrowania lub debugowania.

TypeScript Discriminated Unions

Bardzo często w naszych projektach możemy spotykać się z typem takim jak ten poniżej:

type Order = {
  status: string; // "string" jest dość szerokim typem, a pewnie statusy zamówień są ograniczone do kilku wartości
  name: string;
  description?: string;
  expectedDelivery?: Date; // czemu opcjonalny?
  deliveredOn?: Date; // kiedy opcjonalny, a kiedy nie?
};

Ten sposób definiowania typów jest dość nieczytelny (czemu niektóre właściwości są opcjonalne?) i nieelastyczny. W takich przypadkach warto zastanowić się nad użyciem tzw. Discriminated Unions.

type Order = {
  name: string;
  description?: string;
} & (
  | {
      status: "ready"; // "expectedDelivery" i "deliveredOn" nie powinny tutaj się poajawić
    }
  | {
      status: "inProgress";
      expectedDelivery: Date; // Dostępne wtedy, gdy status jest "inProgress"
    }
  | {
      status: "complete";
      expectedDelivery: Date; // Dostępne wtedy, gdy status jest "complete"
      deliveredOn: Date; // Dostępne wtedy, gdy status jest "complete"
    }
);

Ten zapis może wydawać się bardziej rozwlekły, ale działa już jako prosta dokumentacja domeny, usuwa mnóstwo niejasności i pozwala pisać bardziej przejrzysty kod. Teraz TS będzie w stanie "domyśleć" się, które właściwości są wymagane w zależności od statusu zamówienia.

Rozszerzenie tego tematu można znaleźć w ciekawych artykule: How I ease the next developer reading my code

infer - zaawansowany typ warunkowy w TS

W TypeScript możemy wykorzystać typy warunkowe wraz z słowem kluczowym infer do wnioskowania o typach. Przyjrzyjmy się poniższemu przykładowi:

type ArrayTypes<T> = T extends (infer U)[] ? U : never;
 
let arr = [1, "2", []];
type test = ArrayTypes<typeof arr>; // typ test = string | number | any[]

W tym przykładzie, typ ArrayTypes<T> bierze typ T i sprawdza, czy T jest typem tablicy. Jeśli tak, to inferuje typ U z tablicy T[] i zwraca U. W przeciwnym razie zwraca never.

Ważne jest, aby zauważyć, że infer musi być podłączony do U, a nie do []. Dlatego korzystamy z nawiasów w (infer U)[]. Bez nawiasów, TypeScript zinterpretowałby to jako infer (U[]), co byłoby niepoprawne, ponieważ infer może być używane tylko do inferencji typów, a nie struktur typów takich jak tablice【33†source】.

Kolejny przykład, tym razem z obiektem. W tym przypadku infer stworzy nam typ, który będzie zawierał wszystkie typy wartości obiektu:

type ObjectTypes<T> = T extends { a: infer U; b: infer U } ? U : never;
 
let obj = { a: 1, b: "2" };
type Test = ObjectTypes<typeof obj>; // typ Test = string | number

Jak widać możliwości tutaj są ogromne, więc warto zapoznać się z tym tematem.

Typy warunkowe w TypeScript

Typy warunkowe w TypeScript pozwalają na wybór typu w zależności od warunku. Jest to dość przydatne narzędzie, które pozwala na tworzenie bardziej złożonych typów.

type IsString<T> = T extends string ? true : false;
 
type X = IsString<"hello">; // true
type Y = IsString<number>; // false
type Z = IsString<{ name: string }>; // false

W tym prostym przykładzie, IsString jest typem warunkowym, który sprawdza, czy podany typ T jest stringiem. Jeśli tak, to zwraca true, a w przeciwnym razie zwraca false.

Pamiętaj, że typy warunkowe mogą być zagnieżdżone i mogą używać dowolnej logiki (możemy np. używać tego sprawdzenia w if-ach i zawężać bardziej typowanie), która jest dostępna w typach, co czyni je niezwykle elastycznymi i potężnymi narzędziami do modelowania typów w TypeScript.

unknown vs any w TS

W TypeScript typ unknown jest bezpieczniejszym odpowiednikiem typu any. Podczas gdy any pozwala nam na wszystko, unknown wymusza sprawdzanie typu przed wykonaniem operacji.

let foo: any = "hello";
foo = foo.split("").reverse().join(""); // Zadziała, ale...
 
let foo2: any = { greet: "hello" };
foo2 = foo.split("").reverse().join(""); // TS nie wykryje błędu, ale dostaniemy błąd w konsoli
 
let bar: unknown = "world";
bar = bar.split("").reverse().join(""); // TS podkreśli nam błąd w edytorze, ponieważ nie możemy użyć metody "split" na nieznanym typie.
 
bar = (bar as string).split("").reverse().join(""); // OK, ponieważ zadeklarowaliśmy typ, ale może być lepiej...
bar = typeof bar === "string" ? bar.split("").reverse().join("") : bar; // Jeszcze lepiej, ponieważ sprawdziliśmy typ

Z unknown TypeScript zapewnia, że musiz sprawdzić typ przed wykonaniem operacji, więc jest to dużo bezpieczniejsze podejście niż any.

Aliasy podczas importów w TS

Macie już dość kropkowania podczas importów? Aliasy w TS mogą nam tutaj znacznie uprościć życie. Do pliku tsconfig.json wystarczy dodać następującą konfigurację:

tsconfig.json
{
  "compilerOptions": {
    // Your other options...
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Od teraz możemy zaczynać nasze importy od zdefiniowanego wcześniej poziomu w naszej aplikacji:

import { Button } from "@/components/Button"; // 🤩 import z "src/components/Button"
import { OtherButton } from "../../../components/Button"; // 😥

Taka składnia jest dużo czytelniejsza i łatwiejsza do utrzymania. Jeśli zmienimy zagnieżdżenie bądź położenie folderu, to nie musimy martwić się o zmianę ścieżek w naszym kodzie 💪.

Wartość domyślna dla każdego klucza w obiekcie

Czasami chcemy (niezbyt często, ale może się zdarzyć 😉), aby właściwości obiektu były automatycznie inicjalizowane. Powszechnym sposobem radzenia sobie z tym jest korzystanie z metod getter i setter. Jednak może to stać się uciążliwe, jeśli mamy wiele właściwości. Właśnie tutaj obiekt Proxy może okazać się bardzo pomocny.

Proxy w JavaScript jest używane do definiowania niestandardowego zachowania dla podstawowych operacji (np. wyszukiwanie, przypisanie, wyliczanie, wywoływanie funkcji itp.) obiektów.

Proxy możemy również użyć więc również do automatycznego inicjowania właściwości obiektów, których właściwości nie są do końca znane:

const autoInit = (defaultValue) =>
  new Proxy(
    {},
    {
      get: (target, name) =>
        name in target ? target[name] : (target[name] = defaultValue),
    }
  );
 
const myObject = autoInit([]);
 
console.log(myObject.foo); // []
 
myObject.foo.push("bar");
 
console.log(myObject.foo); // ['bar']

W tym przykładzie każda właściwość myObject, która jest dostępna przed jej ustawieniem, zostanie automatycznie zainicjowana na pustą tablicę (lub dowolną inną domyślną wartość, którą podamy). Może to być dość przydatne w sytuacjach w których chcemy uniknąć wartości undefined.

Szybkie sprawdzanie właściwości obiektu w JavaScript

Czy wiedziałeś, że możesz sprawdzić, czy obiekt ma daną właściwość używając operatora in? Oto jak to zrobić:

const myObject = { foo: "bar" };
 
console.log("foo" in myObject); // true
console.log("baz" in myObject); // false

Szybko i czysto, bez potrzeby sprawdzania undefined 😉

Readonly w TS

W języku TypeScript można użyć modyfikatora readonly, aby wskazać, że właściwość klasy lub interfejsu jest tylko do odczytu, co oznacza, że po przypisaniu do niej wartości nie można jej zmienić. Oto krótki przykład:

interface Point {
  readonly x: number;
  readonly y: number;
}
 
let p: Point = { x: 10, y: 20 };
p.x = 5; // Error: Cannot assign to 'x' because it is a read-only property

W powyższym kodzie x i y w Point są właściwościami tylko do odczytu 💪.

Brak mocków w testach z React Router (v6) i React Testing Library

Customowy render(), za pomocą którego możemy łatwo testować komponenty wykorzystujące nawigację w naszej aplikacji:

import React, { isValidElement } from "react";
import { render } from "@testing-library/react";
import { RouterProvider, createMemoryRouter } from "react-router-dom";
 
export function renderWithRouter(children, routes = []) {
  const options = isValidElement(children)
    ? { element: children, path: "/" }
    : children;
 
  const router = createMemoryRouter([{ ...options }, ...routes], {
    initialEntries: [options.path],
    initialIndex: 1,
  });
 
  return render(<RouterProvider router={router} />);
}

Dokładniejsze omówienie powyższego kodu oraz przykładowe użycie do znalezienia w tym artykule.

TS type guards dla tablic danych

Mając tablicę danych, która może zawierać różne typy (czyli np. nie jesteśmy pewni odpowiedzi z API), to możemy użyć type guard i metodę .every() zamiast if-ów, żeby zawęzić sobie typowanie.

// type guard z użyciem słowa kluczowego "is"
function isArrayOfNumbers(arr: unknown[]): arr is number[] {
  return arr.every((element) => typeof element === "number");
}
 
const arr = [1, 2, 3, 4, 5];
 
if (isArrayOfNumbers(arr)) {
  console.log(arr[0].toFixed); // ✅ OK, wiemy, że "arr" jest tablicą liczb
} else {
  console.log(arr[0].toFixed); // ⛔ ERROR, nie możemy użyć metody ".toFixed()" na typie "unknown"
}

Powyżej mamy prosty przykład z tablicą liczb, ale możemy też użyć tego rozwiązania do bardziej skomplikowanych typów danych.