Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active October 26, 2025 19:56
Show Gist options
  • Save sunmeat/3c1b1c5b896f00e117579ce30fe68a5a to your computer and use it in GitHub Desktop.
Save sunmeat/3c1b1c5b896f00e117579ce30fe68a5a to your computer and use it in GitHub Desktop.
react useRef extended example
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