Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

React Transition Group - Animowanie międzygwiezdnych podróży UI

Poruszanie się między różnymi stanami i widokami w aplikacji kosmicznej powinno być płynne jak lot statku kosmicznego przez hiperprzestrzeń. Nagłe pojawianie się i znikanie elementów interfejsu przypomina raczej awarię systemu niż dobrze zaprojektowane doświadczenie użytkownika. W tej części misji poznamy React Transition Group - bibliotekę, która pomoże nam tworzyć płynne, kosmicznie dobre animacje przejść w naszych interfejsach.

Czym jest React Transition Group?

React Transition Group to niskopoziomowa biblioteka API dla animacji wejścia/wyjścia komponentów. W przeciwieństwie do gotowych rozwiązań animacyjnych, nie dostarcza ona predefiniowanych efektów - zamiast tego daje deweloperom pełną kontrolę nad procesem animacji poprzez zarządzanie klasami CSS lub stylami w kluczowych momentach cyklu życia komponentu.

Jest jak panel kontrolny statku kosmicznego - nie steruje bezpośrednio lotem, ale daje ci narzędzia do precyzyjnego kierowania.

Kluczowe komponenty React Transition Group

1.
Transition

Podstawowy komponent niższego poziomu, który zarządza stanami przejścia komponentu:

1import { Transition } from 'react-transition-group';
2import React, { useState } from 'react';
3
4// Style dla różnych stanów
5const duration = 300;
6const defaultStyle = {
7  transition: `opacity ${duration}ms ease-in-out`,
8  opacity: 0,
9}
10
11const transitionStyles = {
12  entering: { opacity: 1 },
13  entered:  { opacity: 1 },
14  exiting:  { opacity: 0 },
15  exited:   { opacity: 0 },
16};
17
18function FadeExample() {
19  const [inProp, setInProp] = useState(false);
20  
21  return (
22    <div>
23      <button onClick={() => setInProp(!inProp)}>
24        {inProp ? 'Zniknij' : 'Pojaw się'}
25      </button>
26      
27      <Transition in={inProp} timeout={duration}>
28        {state => (
29          <div style={{
30            ...defaultStyle,
31            ...transitionStyles[state]
32          }}>
33            Komunikat z kosmosu
34          </div>
35        )}
36      </Transition>
37    </div>
38  );
39}

2.
CSSTransition

Rozszerza

Transition
o automatyczne dodawanie/usuwanie klas CSS, co ułatwia tworzenie animacji:

1import { CSSTransition } from 'react-transition-group';
2import React, { useState } from 'react';
3import './styles.css';
4
5function AlertExample() {
6  const [showAlert, setShowAlert] = useState(false);
7  
8  return (
9    <div>
10      <button onClick={() => setShowAlert(prev => !prev)}>
11        {showAlert ? 'Ukryj alert' : 'Pokaż alert'}
12      </button>
13      
14      <CSSTransition
15        in={showAlert}
16        timeout={300}
17        classNames="alert"
18        unmountOnExit
19      >
20        <div className="alert">
21          <h4>Komunikat z centrali!</h4>
22          <p>Wykryto nową planetę nadającą się do kolonizacji.</p>
23        </div>
24      </CSSTransition>
25    </div>
26  );
27}

A w pliku CSS:

1/* styles.css */
2.alert {
3  background-color: #121a2b;
4  color: white;
5  padding: 20px;
6  border-radius: 8px;
7  box-shadow: 0 0 20px rgba(82, 161, 255, 0.5);
8  margin-top: 20px;
9}
10
11.alert-enter {
12  opacity: 0;
13  transform: scale(0.9);
14}
15
16.alert-enter-active {
17  opacity: 1;
18  transform: scale(1);
19  transition: opacity 300ms, transform 300ms;
20}
21
22.alert-exit {
23  opacity: 1;
24  transform: scale(1);
25}
26
27.alert-exit-active {
28  opacity: 0;
29  transform: scale(0.9);
30  transition: opacity 300ms, transform 300ms;
31}

3.
TransitionGroup

Zarządza kolekcją komponentów

Transition
lub
CSSTransition
, idealny do animowania list:

1import { TransitionGroup, CSSTransition } from 'react-transition-group';
2import React, { useState } from 'react';
3import './styles.css';
4
5function StarsList() {
6  const [items, setItems] = useState([
7    { id: 1, name: 'Alpha Centauri' },
8    { id: 2, name: 'Proxima Centauri' },
9    { id: 3, name: 'Betelgeuse' }
10  ]);
11  
12  const [nextId, setNextId] = useState(4);
13  
14  const addStar = () => {
15    const newStars = [
16      'Sirius', 'Vega', 'Polaris', 'Canopus', 'Arcturus', 
17      'Antares', 'Rigel', 'Procyon', 'Deneb'
18    ];
19    const randomStar = newStars[Math.floor(Math.random() * newStars.length)];
20    
21    setItems([...items, { id: nextId, name: randomStar }]);
22    setNextId(nextId + 1);
23  };
24  
25  const removeStar = id => {
26    setItems(items.filter(item => item.id !== id));
27  };
28  
29  return (
30    <div>
31      <h2>Gwiazdy do odwiedzenia</h2>
32      <button onClick={addStar}>Dodaj gwiazdę</button>
33      
34      <TransitionGroup className="stars-list">
35        {items.map(({ id, name }) => (
36          <CSSTransition
37            key={id}
38            timeout={500}
39            classNames="star-item"
40          >
41            <div className="star-item">
42              <span>{name}</span>
43              <button onClick={() => removeStar(id)}>
44                &times;
45              </button>
46            </div>
47          </CSSTransition>
48        ))}
49      </TransitionGroup>
50    </div>
51  );
52}

CSS do tego przykładu:

1/* styles.css */
2.stars-list {
3  margin-top: 20px;
4}
5
6.star-item {
7  display: flex;
8  justify-content: space-between;
9  align-items: center;
10  padding: 12px 20px;
11  margin-bottom: 10px;
12  background-color: #0d1b2a;
13  border-radius: 8px;
14  color: #e0f7fa;
15  border-left: 4px solid #4fc3f7;
16}
17
18.star-item-enter {
19  opacity: 0;
20  transform: translateX(-30px);
21}
22
23.star-item-enter-active {
24  opacity: 1;
25  transform: translateX(0);
26  transition: opacity 500ms, transform 500ms;
27}
28
29.star-item-exit {
30  opacity: 1;
31}
32
33.star-item-exit-active {
34  opacity: 0;
35  transform: translateX(30px);
36  transition: opacity 500ms, transform 500ms;
37}

Zaawansowane techniki

1. Animacje sekwencyjne

Animowanie elementów jeden po drugim dla efektu kaskadowego:

1import { CSSTransition, TransitionGroup } from 'react-transition-group';
2import React, { useState, useEffect } from 'react';
3import './styles.css';
4
5function LaunchSequence() {
6  const [steps, setSteps] = useState([]);
7  const allSteps = [
8    { id: 1, text: "Uruchamianie systemów" },
9    { id: 2, text: "Kontrola silników głównych" },
10    { id: 3, text: "Sprawdzanie systemów nawigacyjnych" },
11    { id: 4, text: "Zamykanie włazów" },
12    { id: 5, text: "Odliczanie do startu" }
13  ];
14  
15  useEffect(() => {
16    if (steps.length < allSteps.length) {
17      const timer = setTimeout(() => {
18        setSteps(prevSteps => [
19          ...prevSteps, 
20          allSteps[prevSteps.length]
21        ]);
22      }, 1000);
23      
24      return () => clearTimeout(timer);
25    }
26  }, [steps]);
27  
28  return (
29    <div className="launch-sequence">
30      <h2>Sekwencja startowa</h2>
31      <TransitionGroup>
32        {steps.map(step => (
33          <CSSTransition
34            key={step.id}
35            timeout={500}
36            classNames="launch-step"
37          >
38            <div className="launch-step">
39              <div className="step-number">{step.id}</div>
40              <div className="step-text">{step.text}</div>
41            </div>
42          </CSSTransition>
43        ))}
44      </TransitionGroup>
45    </div>
46  );
47}

2. Choreografia animacji

Synchronizacja wielu animacji dla stworzenia złożonych sekwencji:

1import { Transition } from 'react-transition-group';
2import React, { useState } from 'react';
3
4function SpaceshipDashboard() {
5  const [active, setActive] = useState(false);
6  
7  // Czasy trwania dla różnych elementów
8  const durations = {
9    background: 500,
10    systems: 400,
11    controls: 600,
12    status: 700,
13  };
14  
15  // Style dla różnych stanów
16  const getStyles = (elementName, state) => {
17    const baseStyles = {
18      transition: `all ${durations[elementName]}ms ease-out`,
19    };
20    
21    const elementSpecificStyles = {
22      background: {
23        entering: { opacity: 1, filter: 'brightness(1)' },
24        entered: { opacity: 1, filter: 'brightness(1)' },
25        exiting: { opacity: 0.5, filter: 'brightness(0.4)' },
26        exited: { opacity: 0.5, filter: 'brightness(0.4)' },
27      },
28      systems: {
29        entering: { opacity: 1, transform: 'translateY(0)' },
30        entered: { opacity: 1, transform: 'translateY(0)' },
31        exiting: { opacity: 0, transform: 'translateY(20px)' },
32        exited: { opacity: 0, transform: 'translateY(20px)' },
33      },
34      controls: {
35        entering: { opacity: 1, transform: 'scale(1)' },
36        entered: { opacity: 1, transform: 'scale(1)' },
37        exiting: { opacity: 0, transform: 'scale(0.8)' },
38        exited: { opacity: 0, transform: 'scale(0.8)' },
39      },
40      status: {
41        entering: { opacity: 1, transform: 'translateX(0)' },
42        entered: { opacity: 1, transform: 'translateX(0)' },
43        exiting: { opacity: 0, transform: 'translateX(-20px)' },
44        exited: { opacity: 0, transform: 'translateX(-20px)' },
45      },
46    };
47    
48    return {
49      ...baseStyles,
50      ...elementSpecificStyles[elementName][state],
51    };
52  };
53  
54  // Początkowe style dla elementów
55  const initialStyles = {
56    background: { opacity: 0.5, filter: 'brightness(0.4)' },
57    systems: { opacity: 0, transform: 'translateY(20px)' },
58    controls: { opacity: 0, transform: 'scale(0.8)' },
59    status: { opacity: 0, transform: 'translateX(-20px)' },
60  };
61  
62  return (
63    <div className="dashboard-container">
64      <button onClick={() => setActive(!active)}>
65        {active ? 'Wyłącz systemy' : 'Uruchom systemy'}
66      </button>
67      
68      <div className="spaceship-dashboard">
69        <Transition in={active} timeout={durations.background}>
70          {state => (
71            <div 
72              className="dashboard-background"
73              style={{
74                ...initialStyles.background,
75                ...getStyles('background', state)
76              }}
77            />
78          )}
79        </Transition>
80        
81        <Transition in={active} timeout={durations.systems}>
82          {state => (
83            <div 
84              className="system-panels"
85              style={{
86                ...initialStyles.systems,
87                ...getStyles('systems', state)
88              }}
89            >
90              System Diagnostics
91            </div>
92          )}
93        </Transition>
94        
95        <Transition in={active} timeout={durations.controls}>
96          {state => (
97            <div 
98              className="control-panels"
99              style={{
100                ...initialStyles.controls,
101                ...getStyles('controls', state)
102              }}
103            >
104              Flight Controls
105            </div>
106          )}
107        </Transition>
108        
109        <Transition in={active} timeout={durations.status}>
110          {state => (
111            <div 
112              className="status-panels"
113              style={{
114                ...initialStyles.status,
115                ...getStyles('status', state)
116              }}
117            >
118              Status Monitors
119            </div>
120          )}
121        </Transition>
122      </div>
123    </div>
124  );
125}

Dobre praktyki dla kosmicznie płynnych animacji

  1. Unikaj animowania właściwości kosztownych obliczeniowo - właściwości takie jak

    width
    ,
    height
    i
    top
    mogą powodować reflow. Preferuj
    transform
    i
    opacity
    .

  2. Używaj debugera animacji - narzędzia takie jak Chrome DevTools pozwalają analizować wydajność animacji.

  3. Przemyśl punkty początku i końca - dobrze zaprojektowana animacja ma jasno określone stany.

  4. Nie przesadzaj z czasem trwania - animacje powinny być wystarczająco krótkie, aby nie opóźniać interakcji użytkownika (zazwyczaj 150-500ms).

  5. Dodaj opóźnienia dla złożonych sekwencji - używaj

    delay
    aby stworzyć kaskadowe efekty.

  6. Zadbaj o dostępność - pamiętaj o użytkownikach, którzy mogą preferować ograniczone animacje (

    prefers-reduced-motion
    ).

Przykład: Portal międzygwiezdny

Stwórzmy komponent portalu, który animuje przejście między "światami" (widokami) w naszej kosmicznej aplikacji:

1import { CSSTransition } from 'react-transition-group';
2import React, { useState } from 'react';
3import './portal.css';
4
5function InterstellarPortal({ children, show, onExited }) {
6  return (
7    <CSSTransition
8      in={show}
9      timeout={1000}
10      classNames="portal"
11      unmountOnExit
12      onExited={onExited}
13    >
14      <div className="portal-container">
15        <div className="portal-content">
16          {children}
17        </div>
18      </div>
19    </CSSTransition>
20  );
21}
22
23function App() {
24  const [currentWorld, setCurrentWorld] = useState('home');
25  const [showPortal, setShowPortal] = useState(true);
26  const [nextWorld, setNextWorld] = useState(null);
27  
28  const worlds = {
29    home: {
30      title: "Stacja Kosmiczna Alpha",
31      content: "Witaj na pokładzie stacji kosmicznej Alpha, centrum dowodzenia naszej międzygwiezdnej operacji."
32    },
33    mars: {
34      title: "Mars Colony One",
35      content: "Witaj w pierwszej kolonii na Marsie. Prace terraformacyjne są w toku."
36    },
37    europa: {
38      title: "Podwodna baza Europa",
39      content: "Witaj w bazie badawczej pod lodową powłoką księżyca Jowisza - Europy."
40    }
41  };
42  
43  const navigateTo = (world) => {
44    setNextWorld(world);
45    setShowPortal(false);
46  };
47  
48  const onPortalExited = () => {
49    setCurrentWorld(nextWorld);
50    setShowPortal(true);
51  };
52  
53  return (
54    <div className="app">
55      <nav className="navigation">
56        <button onClick={() => navigateTo('home')} disabled={currentWorld === 'home'}>
57          Stacja Alpha
58        </button>
59        <button onClick={() => navigateTo('mars')} disabled={currentWorld === 'mars'}>
60          Mars
61        </button>
62        <button onClick={() => navigateTo('europa')} disabled={currentWorld === 'europa'}>
63          Europa
64        </button>
65      </nav>
66      
67      <InterstellarPortal show={showPortal} onExited={onPortalExited}>
68        <h1>{worlds[currentWorld].title}</h1>
69        <p>{worlds[currentWorld].content}</p>
70      </InterstellarPortal>
71    </div>
72  );
73}

CSS dla portalu:

1/* portal.css */
2.portal-container {
3  position: relative;
4  overflow: hidden;
5  min-height: 300px;
6  padding: 30px;
7  background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
8  border-radius: 12px;
9  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
10}
11
12.portal-content {
13  background: rgba(0, 0, 0, 0.7);
14  padding: 30px;
15  border-radius: 8px;
16  color: white;
17}
18
19/* Animacja wejścia */
20.portal-enter {
21  opacity: 0;
22  transform: scale(0.7);
23}
24
25.portal-enter-active {
26  opacity: 1;
27  transform: scale(1);
28  transition: opacity 1000ms ease-out, transform 1000ms cubic-bezier(0.34, 1.56, 0.64, 1);
29}
30
31/* Animacja wyjścia */
32.portal-exit {
33  opacity: 1;
34  transform: scale(1);
35}
36
37.portal-exit-active {
38  opacity: 0;
39  transform: scale(1.3);
40  transition: opacity 1000ms ease-in, transform 1000ms cubic-bezier(0.36, 0, 0.66, -0.56);
41}
42
43/* Styl dla nawigacji */
44.navigation {
45  display: flex;
46  justify-content: center;
47  gap: 15px;
48  margin-bottom: 30px;
49}
50
51.navigation button {
52  background-color: #2c3e50;
53  color: white;
54  border: none;
55  padding: 10px 15px;
56  border-radius: 5px;
57  cursor: pointer;
58  transition: all 0.3s;
59}
60
61.navigation button:hover:not(:disabled) {
62  background-color: #34495e;
63  transform: translateY(-2px);
64}
65
66.navigation button:disabled {
67  background-color: #95a5a6;
68  cursor: not-allowed;
69}

Zadanie praktyczne: Stwórz animowaną galerię planet

Zaprojektuj i zaimplementuj animowaną galerię planet w naszym układzie słonecznym. Kiedy użytkownik nawiguje między planetami, użyj React Transition Group, aby stworzyć płynne przejścia między widokami. Dla każdej planety wyświetl:

  1. Nazwę planety
  2. Obrazek planety
  3. Kilka kluczowych faktów (rozmiar, odległość od Słońca, temperatura)

Dodaj następujące animacje:

  • Płynne przejście między planetami
  • Efekt kaskadowy dla wyświetlania faktów o planecie
  • Przycisk "Powrót do przeglądu" z animacją przejścia

Pamiętaj, że dobra animacja wzmacnia doświadczenie użytkownika, nie rozprasza go. Animacje powinny być płynne i naturalne, jak sama podróż przez kosmos - elegancka, majestatyczna i inspirująca.

Przejdź do CodeWorlds