Last active
          October 26, 2025 19:56 
        
      - 
      
- 
        Save sunmeat/3c1b1c5b896f00e117579ce30fe68a5a to your computer and use it in GitHub Desktop. 
    react useRef extended example
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | App.jsx: | |
| // npm install framer-motion chart.js react-chartjs-2 three @react-three/fiber | |
| import React, {useRef, useEffect, useState} from 'react'; | |
| import {motion, useAnimation} from 'framer-motion'; // motion використовується неявно для створення анімації, тому ESLint йде геть :) | |
| import {Canvas, useFrame} from '@react-three/fiber'; | |
| import * as THREE from 'three'; // імпортуємо весь модуль бібліотеки Three.js і присвоюємо його об’єкту з ім’ям THREE | |
| import {Line} from 'react-chartjs-2'; | |
| import { | |
| Chart as ChartJS, | |
| LineElement, | |
| PointElement, | |
| LinearScale, | |
| CategoryScale, | |
| Title, | |
| Tooltip, | |
| Legend | |
| } from 'chart.js'; | |
| import './App.css'; | |
| // реєстрація компонентів Chart.js | |
| ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Title, Tooltip, Legend); | |
| // компонент кубіка з різнокольоровими сторонами | |
| function Cube({rotationSpeed}) { | |
| const meshRef = useRef(null); | |
| // !!! важливо: useRef тут потрібен для доступу до mesh без ререндера | |
| useFrame(() => { | |
| if (meshRef.current) { | |
| meshRef.current.rotation.x += rotationSpeed; | |
| meshRef.current.rotation.y += rotationSpeed; | |
| } | |
| }); | |
| // https://threejs.org/ | |
| // https://r3f.docs.pmnd.rs/getting-started/introduction?trk=public_post_comment-text | |
| // масив матеріалів для кожної сторони куба | |
| const materials = [ | |
| new THREE.MeshStandardMaterial({color: '#a3bffa'}), // пастельний синій | |
| new THREE.MeshStandardMaterial({color: '#fabed4'}), // пастельний рожевий | |
| new THREE.MeshStandardMaterial({color: '#ffd1a9'}), // персиковий | |
| new THREE.MeshStandardMaterial({color: '#c3e6cb'}), // м’ятний | |
| new THREE.MeshStandardMaterial({color: '#f6c1ff'}), // лавандовий | |
| new THREE.MeshStandardMaterial({color: '#b5e3e3'}), // бірюзовий | |
| ]; | |
| return ( | |
| <mesh ref={meshRef}> | |
| <boxGeometry args={[1, 1, 1]}/> | |
| {materials.map((material, index) => ( | |
| <meshStandardMaterial key={index} attach={`material-${index}`} {...material} /> | |
| ))} | |
| </mesh> | |
| ); | |
| } | |
| const App = () => { | |
| // !!! тут useRef потрібен для зберігання таймера без ререндера | |
| const timerRef = useRef(null); | |
| // !!! а тут для зберігання екземпляра графіка Chart.js | |
| const chartInstanceRef = useRef(null); | |
| // !!! для керування швидкістю обертання куба без ререндера | |
| const cubeSpeedRef = useRef(0.01); | |
| // керування анімацією Framer Motion | |
| const controls = useAnimation(); | |
| // стан для часу таймера | |
| const [timerMessage, setTimerMessage] = useState('00:00:00.000'); | |
| // стан даних графіка (курс біткоїна) | |
| const [chartData, setChartData] = useState({ | |
| labels: [], | |
| datasets: [ | |
| { | |
| label: 'Курс біткоїна (USD)', | |
| data: [], | |
| borderColor: '#4299e1', | |
| fill: false, | |
| }, | |
| ], | |
| }); | |
| // стан для причини зміни ціни | |
| const [priceReason, setPriceReason] = useState(''); | |
| // установка таймера для формату ГГ:ХХ:СС.мсмс | |
| useEffect(() => { | |
| const startTime = Date.now(); | |
| timerRef.current = setInterval(() => { | |
| const elapsed = Date.now() - startTime; | |
| const hours = Math.floor(elapsed / (1000 * 60 * 60)).toString().padStart(2, '0'); | |
| const minutes = Math.floor((elapsed % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0'); | |
| const seconds = Math.floor((elapsed % (1000 * 60)) / 1000).toString().padStart(2, '0'); | |
| const milliseconds = (elapsed % 1000).toString().padStart(3, '0'); | |
| setTimerMessage(`${hours}:${minutes}:${seconds}.${milliseconds}`); | |
| }, 10); // оновлення кожні 10 мс для плавності | |
| // !!! .current: очищення таймера при розмонтуванні | |
| return () => clearInterval(timerRef.current); | |
| }, []); | |
| // очищення екземпляра графіка перед новим рендером | |
| useEffect(() => { | |
| // !!! знищення старого графіка для уникнення конфлікту canvas | |
| return () => { | |
| if (chartInstanceRef.current) { | |
| chartInstanceRef.current.destroy(); | |
| chartInstanceRef.current = null; | |
| } | |
| }; | |
| }, []); | |
| // завантаження та оновлення даних курсу біткоїна | |
| useEffect(() => { | |
| // моковані історичні дані (підключати API було лінь :)) | |
| const mockHistoricalData = { | |
| prices: [ | |
| [1420070400000, 300], // 2015 | |
| [1451606400000, 430], // 2016 | |
| [1483228800000, 13700], // 2017 | |
| [1514764800000, 3800], // 2018 | |
| [1546300800000, 7200], // 2019 | |
| [1577836800000, 29000], // 2020 | |
| [1609459200000, 47000], // 2021 | |
| [1640995200000, 16500], // 2022 | |
| [1672531200000, 26000], // 2023 | |
| [1704067200000, 42000], // 2024 | |
| [1735689600000, 108000], // 2025 | |
| ], | |
| }; | |
| // ініціалізація графіка історичними даними | |
| setChartData({ | |
| labels: mockHistoricalData.prices.map(([timestamp]) => | |
| new Date(timestamp).getFullYear().toString() | |
| ), | |
| datasets: [ | |
| { | |
| label: 'Курс біткоїна (USD)', | |
| data: mockHistoricalData.prices.map(([, price]) => price), | |
| borderColor: '#4299e1', | |
| fill: false, | |
| }, | |
| ], | |
| }); | |
| // масив причин для зміни ціни | |
| const funnyReasons = [ | |
| 'Ілон Маск знову чхнув на iPhone, і вийшов твіт про біткоїн', | |
| 'Кіт Сатоші стрибнув на клавіатуру', | |
| 'Дональд Трамп заявив, що купив чизбургер за біткоїн', | |
| 'Хом’яки постриглися', | |
| 'Дикій огірок', | |
| 'Майнери знайшли золоту жилу в одеському трамвайному депо', | |
| 'Бабусі навчилися майнити BTC на банках з закрученими дикими огірками', | |
| 'Хакери зламали гаманець, але знайшли лише купон на піцу', | |
| 'Криптобіржа оголосила розпродаж "Чорна п’ятниця"', | |
| 'Крипто-інфлюенсер запостив смайлик 🚀, і ринок вибухнув', | |
| 'Майнери випадково підключили ферму до електрочайника!', | |
| 'Криптоботи влаштували флешмоб у Telegram', | |
| 'Ілон запустив ракету з логотипом BTC', | |
| 'Нейро-котик у TikTok показав, як майнити на лапках', | |
| 'Майнери знайшли монету BTC у старому дивані', | |
| 'Крипто-гуру передбачив зростання на безлактозно-кавовій гущі', | |
| 'Біткоїн упав через баг у грі CryptoKitties', | |
| 'Ілон Маск назвав біткоїн "валютою Марса"', | |
| 'Трейдери вирішили майнити на сонячних батареях', | |
| 'Біткоїн упав, тому що хтось сказав "фіат"', | |
| 'Біткоїн упав через збій Wi-Fi на центральній біржі Тирасполя', | |
| 'Майнери випадково підключилися до сусідської розетки', | |
| 'Криптобіржа дала збій через атаку розлючених стрижених хом’яків', | |
| ]; | |
| // динамічне оновлення ціни кожну секунду | |
| const priceUpdateInterval = setInterval(() => { | |
| setChartData((prev) => { | |
| const newPrice = prev.datasets[0].data[prev.datasets[0].data.length - 1] * ( | |
| Math.random() > 0.2 ? 1.02 : 0.95 | |
| ); // 80% зростання, 20% падіння | |
| const newData = [...prev.datasets[0].data, newPrice.toFixed(2)]; | |
| const newLabels = [...prev.labels, new Date().toLocaleTimeString('uk-UA')]; | |
| setPriceReason(funnyReasons[Math.floor(Math.random() * funnyReasons.length)]); | |
| return { | |
| labels: newLabels, | |
| datasets: [ | |
| { | |
| ...prev.datasets[0], | |
| data: newData, | |
| }, | |
| ], | |
| }; | |
| }); | |
| }, 1000); | |
| return () => clearInterval(priceUpdateInterval); | |
| }, []); | |
| // !!! імперативна анімація кнопки | |
| const handleButtonClick = async () => { | |
| cubeSpeedRef.current = cubeSpeedRef.current === 0.01 ? 0.05 : 0.01; | |
| await controls.start({ | |
| scale: 1.2, | |
| rotate: 360, | |
| transition: {duration: 0.5}, | |
| }); | |
| await controls.start({ | |
| scale: 1, | |
| rotate: 0, | |
| transition: {duration: 0.5}, | |
| }); | |
| }; | |
| return ( | |
| <div className="container"> | |
| <h1 className="title">useRef + анімації та бібліотеки</h1> | |
| {/* three.js сцена */} | |
| <div className="canvas-container"> | |
| <Canvas> | |
| <ambientLight intensity={0.5}/> | |
| <pointLight position={[10, 10, 10]}/> | |
| <Cube rotationSpeed={cubeSpeedRef.current}/> | |
| </Canvas> | |
| </div> | |
| {/* chart.js графік */} | |
| <div className="chart-container"> | |
| <Line | |
| ref={(el) => { | |
| if (el && el.chartInstance) { | |
| chartInstanceRef.current = el.chartInstance; | |
| } | |
| }} | |
| data={chartData} | |
| options={{ | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: false, | |
| title: { | |
| display: true, | |
| text: 'Ціна (USD)', | |
| }, | |
| }, | |
| x: { | |
| title: { | |
| display: true, | |
| text: 'Час', | |
| }, | |
| }, | |
| }, | |
| }} | |
| /> | |
| </div> | |
| {/* вивід причини зміни ціни */} | |
| <p className="text">{priceReason || 'Очікую зміни ціни...'}</p> | |
| {/* кнопка для запуску анімації */} | |
| <motion.button | |
| className="button" | |
| onClick={handleButtonClick} | |
| animate={controls} | |
| > | |
| змінити швидкість куба | |
| </motion.button> | |
| {/* вивід часу таймера */} | |
| <p className="text">{timerMessage}</p> | |
| </div> | |
| ); | |
| }; | |
| export default App; | |
| ================================================================================================================= | |
| App.css: | |
| body { | |
| margin: 0; | |
| font-family: sans-serif; | |
| background-color: #edf2f7; | |
| padding: 40px; | |
| } | |
| .container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| background: #ffffff; | |
| padding: 24px; | |
| border-radius: 10px; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | |
| text-align: center; | |
| } | |
| .title { | |
| font-size: 28px; | |
| margin-bottom: 20px; | |
| color: #2d3748; | |
| } | |
| .text { | |
| font-size: 16px; | |
| color: #4a5568; | |
| margin: 10px 0; | |
| } | |
| .canvas-container { | |
| width: 100%; | |
| height: 300px; | |
| margin: 20px 0; | |
| border: 2px solid #4299e1; | |
| border-radius: 6px; | |
| overflow: hidden; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 300px; | |
| margin: 20px 0; | |
| padding: 10px; | |
| border: 2px dashed #4299e1; | |
| border-radius: 6px; | |
| } | |
| .button { | |
| padding: 12px 24px; | |
| font-size: 16px; | |
| background-color: #4299e1; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| transition: background-color 0.3s; | |
| } | |
| .button:hover { | |
| background-color: #2b6cb0; | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment