npx create-react-app react-masterclass --use-npm
npm i styled-components
const BoxOne = styled.div`
background-color: teal;
width: 100px;
height: 100px;
`;
const Text = styled.span`
color: white;
`;
..
<BoxOne>
<Text>Hello</Text>
</BoxOne>
- Adapting
const Box = styled.div`
background-color: ${(props) => props.bgColor};
width: 100px;
height: 100px;
`;
..
<Box bgColor="teal" />
- Extending
const Circle = styled(Box)`
border-radius: 50px;
`;
..
<Circle bgColor="tomato" />
- convert styledComponent to other element by
as
const Btn = styled.button`
color: white;
background-color: tomato;
border: 0;
border-radius: 15px;
`;
..
<Btn as="a" href="/"> {/* convert from button to anchor */}
go home
/Btn>
- give default attributes
const Input = styled.input.attrs({ required: true, minLength: 2 })`
background-color: tomato;
`;
..
<Input />
<Input />
- Animations
const rotationAnimation = keyframes`
0% {
transform:rotate(0deg);
border-radius:0px;
}
50% {
border-radius:100px;
}
100%{
transform:rotate(360deg);
border-radius:0px;
}
`;
..
const Box = styled.div`
animation: ${rotationAnimation} 1s linear infinite;
- Pseudo selectors
- can use html tag inside of styled component to refer to specific child
- & means myself
const Box = styled.div`
span {
font-size: 36px;
&:hover {
font-size: 48px;
}
&:active {
opacity: 0;
}
}
`;
..
<Wrapper>
<Box>
<span>🤩</span>
</Box>
</Wrapper>
- can use styled component name as selector => independant on html tag
const Box = styled.div`
${Emoji}:hover {
font-size: 98px;
}
`;
..
<Box>
<Emoji>🤩</Emoji>
</Box>
// index.js
const darkTheme = {
textColor: "whitesmoke",
backgroundColor: "#111",
};
const lightTheme = {
textColor: "#111",
backgroundColor: "whitesmoke",
};
<ThemeProvider theme={darkTheme}>
<App />
</ThemeProvider>;
// App.js
const Title = styled.h1`
color: ${(props) => props.theme.textColor};
`;
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
mv src/App.js src/App.tsx
mv src/index.js src/index.tsx
npx tsc --init
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
// src/index.tsx
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
- looks like below types are included in styled-components now
npm i --save-dev @types/styled-components
import styled from "styled-components";
interface CircleProps {
bgColor: string;
}
const Container = styled.div<CircleProps>`
width: 200px;
height: 200px;
background-color: ${(props) => props.bgColor};
border-radius: 100px;
`;
function Circle({ bgColor }: CircleProps) {
return <Container bgColor={bgColor} />;
}
export default Circle;
interface CircleProps {
bgColor: string;
borderColor?: string;
text?: string;
}
function Circle({ bgColor, borderColor, text = "default text" }: CircleProps) {
const [value, setValue] = useState<string | number>("");
setValue(1);
const [value, setValue] = useState("");
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const {
currentTarget: { value },
} = event;
setValue(value);
};
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log("value:", value);
};
..
<form onSubmit={onSubmit}>
<input
value={value}
onChange={onChange}
type="text"
placeholder="username"
/>
<button>Log in</button>
</form>
..
- extend module
// touch src/styled.d.ts
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
textColor: string;
bgColor: string;
btnColor: string;
}
}
- configure theme
// touch src/theme.ts
import { DefaultTheme } from "styled-components";
export const lightTheme: DefaultTheme = {
bgColor: "white",
textColor: "black",
btnColor: "tomato",
};
export const darkTheme: DefaultTheme = {
bgColor: "black",
textColor: "white",
btnColor: "teal",
};
- provide theme
// src/index.tsx
<ThemeProvider theme={darkTheme}>
<App />
</ThemeProvider>
- use theme
const Container = styled.div`
background-color: ${(props) => props.theme.bgColor};
`;
const H1 = styled.h1`
color: ${(props) => props.theme.textColor};
`;
..
<Container>
<H1>protected</H1>
</Container>
..
npm i react-router-dom@5.3.4
npm i --save-dev @types/react-router
npm i --save-dev @types/react-router-dom
mkdir -p src/routes
touch src/routes/Coin.tsx
touch src/routes/Coins.tsx
touch src/Router.tsx
function Router() {
return (
<BrowserRouter>
<Switch>
<Route path="/:coinId">
<Coin />
</Route>
<Route path="/">
<Coins />
</Route>
</Switch>
</BrowserRouter>
);
}
-
reset style with pasting this to createGlobalStyle
- or use package: styled-reset
// src/App.tsx
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400&display=swap');
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu, ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Source Sans Pro', sans-serif;
background-color:${(props) => props.theme.bgColor};
color:${(props) => props.theme.textColor}
}
a {
text-decoration:none;
}
`;
function App() {
return (
<>
<GlobalStyle />
<Router />
</>
);
}
- font setup
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400&display=swap');
..
body {
font-family: 'Source Sans Pro', sans-serif;
- color palette: https://flatuicolors.com/palette/gb
- Link to
<Link to={`/${coin.id}`}>{coin.name} →</Link>
- fetch API
useEffect(() => {
(async () => {
const response = await fetch("https://api.coinpaprika.com/v1/coins");
const json = await response.json();
setCoins(json.slice(0, 100));
setLoading(false);
})();
}, []);
- send state by Link
- if access directly without send state => Loading
// Coins.tsx
<Link
to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
}}
>
// Coin.tsx
interface RouteParams {
coinId: string;
}
interface RouteState {
name: string;
}
..
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
useEffect(() => {
(async () => {
const infoData = await (
await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)
).json();
const priceData = await (
await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
).json();
setInfo(infoData);
setPriceInfo(priceData);
})();
}, []);
- type inference workaround: dev console => Store object as global variable
console.log(
Object.entries(temp1)
.map((a) => `${a[0]}: ${typeof a[1]};`)
.join("\r\n")
);
touch src/routes/Price.tsx
touch src/routes/Chart.tsx
- nested routes render components which fit with route
// Coin.tsx
<Switch>
<Route path={`/${coinId}/price`}>
<Price />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart />
</Route>
</Switch>
- check if it's that route with useRouteMatch
const priceMatch = useRouteMatch("/:coinId/price");
const chartMatch = useRouteMatch("/:coinId/chart");
..
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>;
- /:coinId parse the first keyword
<Route path={`/:coinId/price`}>
npm i react-query
- wrap with QueryClientProvider
// src/index.tsx
const queryClient = new QueryClient();
..
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</QueryClientProvider>
- define fetcher
// touch src/api.ts
export function fetchCoins() {
return fetch("https://api.coinpaprika.com/v1/coins").then((response) =>
response.json()
);
}
- fetch data
const { isLoading, data } = useQuery<ICoin[]>("allCoins", fetchCoins);
- ReactQueryDevtools shows cached data
// src/App.tsx
..
<Router />
<ReactQueryDevtools initialIsOpen={false} />
- solve unknown prop "isActive" warning
// src/App.tsx
<>
<StyleSheetManager shouldForwardProp={(prop) => prop !== "isActive"}>
..
</StyleSheetManager>
</>
npm install --save react-apexcharts apexcharts
// src/routes/Chart.tsx
<ApexChart
type="line"
series={[
{
name: "Price",
data: data?.map((price) => price.close),
},
]}
options={{
theme: {
mode: "dark",
},
chart: {
height: 300,
width: 500,
toolbar: {
show: false,
},
background: "transparent",
},
grid: { show: false },
stroke: {
curve: "smooth",
width: 4,
},
yaxis: {
show: false,
},
xaxis: {
axisBorder: { show: false },
axisTicks: { show: false },
labels: { show: false },
},
}}
/>
fill: {
type: "gradient",
gradient: { gradientToColors: ["#0be881"], stops: [0, 100] },
},
..
tooltip: {
y: {
formatter: (value) => `$${value.toFixed(2)}`,
},
},
npm i react-helmet @types/react-helmet
- refetchInterval
const { isLoading: tickersLoading, data: tickersData } = useQuery<PriceData>(
["tickers", coinId],
() => fetchCoinTickers(coinId),
{
refetchInterval: 5000,
}
);
- make go back button
- render prices
- replace barChart to candlestickChart
- should move Provider from
index.tsx
toApp.tsx
to use theme with state
// src/App.tsx
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<button onClick={toggleDark}>Toggle Mode</button>
..
</ThemeProvider>
- nested props make redundant inheritances
- only few children needs state or modification
App (isDark, setIsDark)
=> Router => Coins(setIsDark)
=> Router => Coin => Chart(isDark)
npm install recoil
// touch src/atoms.ts
import { atom } from "recoil";
export const isDarkAtom = atom({
key: "isDark",
default: true,
});
// src/index.tsx
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</RecoilRoot>
// src/App.tsx
const isDark = useRecoilValue(isDarkAtom);
- Any component can acces to the state
App -> (isDark) <- App.Router.Coin.Chart
const setDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setDarkAtom((prev) => !prev);
..
<button onClick={toggleDarkAtom}>Toggle Mode</button>
- rearrange apps
npm i react-hook-form
// src/ToDoList.tsx
const { register, watch } = useForm();
console.log(watch());
..
<form>
<input {...register("email")} placeholder="Email" />
..
- handleSubmit
- form validation: required, minLength, custom message.. by
formState.errors
// src/ToDoList.tsx
<input
{...register("password1", {
required: "Password is required",
minLength: {
value: 5,
message: "Your password is too short.",
},
})}
placeholder="Password1"
/>
// src/ToDoList.tsx
<span>{errors?.password1?.message}</span>
- set message and shouldFocus on error
- for existing field
- for extra field like
server offline
const onValid = (data: IForm) => {
if (data.password !== data.password1) {
setError(
"password1",
{ message: "Password are not the same" },
{ shouldFocus: true }
);
}
setError("extraError", { message: "Server offline." });
};
- invalid: return false or error message
- valid: return true
- type field becomes
validate
by default
validate: (value) => bool | string;
- set specific type
validate: {
validateType: (value) => bool | string;
}
// src/ToDoList.tsx
<input
{...register("firstName", {
required: "write here",
// validate: (value) => "error",
validate: {
noNico: (value) => (value.includes("nico") ? "no nicos allowed" : true),
noNick: (value) => (value.includes("nick") ? "no nick allowed" : true),
},
})}
placeholder="First Name"
/>
- clear practices and start to write todo
mkdir src/components
mv src/routes/Coin src/components
mv src/routes/Todo src/components
- useRecoilState = useRecoilValue + useSetRecoilState
// const toDos = useRecoilValue(toDoState);
// const setToDos = useSetRecoilState(toDoState);
const [toDos, setToDos] = useRecoilState(toDoState);
- fix urls of coin:
/:coinId/chart => /coin/:coinId/chart
- split each apps into
/apps
- split from
ToDoList
toCreateTodo
andTodo
- add callback to get target onClick
- find targetIndex by
findIndex
// Todo.tsx
const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
- create new array with
slice
// Todo.tsx
const newToDo = { text, id, category: name as any };
return [
...oldToDos.slice(0, targetIndex),
newToDo,
...oldToDos.slice(targetIndex + 1),
];
- selector like Computed field
// src/apps/Todo/atoms.tsx
export const toDoSelector = selector({
key: "toDoSelector",
get: ({ get }) => {
const toDos = get(toDoState);
return [
toDos.filter((toDo) => toDo.category === "TO_DO"),
toDos.filter((toDo) => toDo.category === "DOING"),
toDos.filter((toDo) => toDo.category === "DONE"),
];
},
});
// src/apps/Todo/components/ToDoList.tsx
const [toDo, doing, done] = useRecoilValue(toDoSelector);
..
- can filter by another state
export const categoryState = atom({
key: "category",
default: "TO_DO",
});
export const toDoSelector = selector({
key: "toDoSelector",
get: ({ get }) => {
const toDos = get(toDoState);
const category = get(categoryState);
return toDos.filter((toDo) => toDo.category === category);
},
- wrap category with enum
export enum Categories {
"TO_DO" = "TO_DO",
"DOING" = "DOING",
"DONE" = "DONE",
}
export const categoryState = atom<Categories>({
key: "category",
default: Categories.TO_DO,
});
- computed field for hour from minutes
export const hourSelector = selector({
key: "hours",
get: ({ get }) => {
const minutes = get(minuteState);
return minutes / 60;
},
});
- setState with selector
export const hourSelector = selector<number>({
..
set: ({ set }, newValue) => {
const minutes = Number(newValue) * 60;
set(minuteState, minutes);
},
});
npm i react-beautiful-dnd@^13.1.0 @types/react-beautiful-dnd@13.1.2
mv src/apps/Trello/Trello.tsx src/apps/Trello/StudySelector.tsx
touch src/apps/Trello/StudyDrag.tsx
- should remove StrictMode at
src/index.tsx
export default function StudyDrag() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<div>
<Droppable droppableId="one">
{(magic) => (
<ul ref={magic.innerRef} {...magic.droppableProps}>
<Draggable draggableId="first" index={0}>
{(magic) => (
<li ref={magic.innerRef} {...magic.draggableProps}>
<span {...magic.dragHandleProps}>🔥</span>
One
</li>
)}
</Draggable>
<Draggable draggableId="second" index={1}>
{(magic) => (
<li ref={magic.innerRef} {...magic.draggableProps}>
<span {...magic.dragHandleProps}>🔥</span>
Two
</li>
)}
</Draggable>
</ul>
)}
</Droppable>
</div>
</DragDropContext>
);
}
- chore: impl styled components, and change dark theme
- placeholder can keep the place even after dragging
<Droppable droppableId="one">
{(magic) => (
<Board ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<Draggable draggableId={toDo} index={index}>
{(magic) => (
<Card
ref={magic.innerRef}
{...magic.dragHandleProps}
{...magic.draggableProps}
>
{toDo}
</Card>
)}
</Draggable>
))}
{magic.placeholder}
</Board>
)}
</Droppable>
- onDragEnd gived source and destination
- Draggable
key
should be identical withdraggableId
const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
if (!destination) return;
setToDos((oldToDos) => {
const toDosCopy = [...oldToDos];
// 1) Delete item on source.index
console.log("Delete item on", source.index);
console.log(toDosCopy);
toDosCopy.splice(source.index, 1);
console.log("Deleted item");
console.log(toDosCopy);
// 2) Put back the item on the destination.index
console.log("Put back", draggableId, "on ", destination.index);
toDosCopy.splice(destination?.index, 0, draggableId);
console.log(toDosCopy);
return toDosCopy;
});
};
..
<DragDropContext onDragEnd={onDragEnd}>
<Draggable key={toDo} draggableId={toDo} index={index}>
- extract
Components/DragabbleCard.tsx
- Optimize with
React.memo
=> rerender only mutated
export default React.memo(DragabbleCard);
- extract
Components/Board.tsx
- handle case when droppableID is identical
const onDragEnd = (info: DropResult) => {
console.log(info);
const { destination, draggableId, source } = info;
if (destination?.droppableId === source.droppableId) {
// same board movement.
setToDos((allBoards) => {
const boardCopy = [...allBoards[source.droppableId]];
boardCopy.splice(source.index, 1);
boardCopy.splice(destination?.index, 0, draggableId);
return {
...allBoards,
[source.droppableId]: boardCopy,
};
});
}
};
- handle case when destination.droppableId !== source.droppableId
if (destination.droppableId !== source.droppableId) {
// cross board movement
setToDos((allBoards) => {
const sourceBoard = [...allBoards[source.droppableId]];
const destinationBoard = [...allBoards[destination.droppableId]];
sourceBoard.splice(source.index, 1);
destinationBoard.splice(destination?.index, 0, draggableId);
return {
...allBoards,
[source.droppableId]: sourceBoard,
[destination.droppableId]: destinationBoard,
};
});
}
- colorize with isDragginOver & draggingFromThisWith
flex-grow: 1
takes height as much as possible => broad Droppable area
const Area = styled.div<IAreaProps>`
background-color: ${(props) =>
props.isDraggingOver ? "pink" : props.isDraggingFromThis ? "red" : "blue"};
flex-grow: 1;
transition: background-color 0.3s ease-in-out;
`;
..
<Droppable droppableId={boardId}>
{(magic, info) => (
<Area
isDraggingOver={info.isDraggingOver}
isDraggingFromThis={Boolean(info.draggingFromThisWith)}
ref={magic.innerRef}
{...magic.droppableProps}
>
// src/apps/Trello/Components/DragabbleCard.tsx
const Card = styled.div<{ isDragging: boolean }>`
border-radius: 5px;
margin-bottom: 5px;
padding: 10px;
background-color: ${(props) =>
props.isDragging ? "#e4f2ff" : props.theme.cardColor};
box-shadow: ${(props) =>
props.isDragging ? "0px 2px 5px rgba(0, 0, 0, 0.05)" : "none"};
`;
// src/apps/Trello/Components/Board.tsx
const Area = styled.div<IAreaProps>`
background-color: ${(props) =>
props.isDraggingOver
? "#dfe6e9"
: props.isDraggingFromThis
? "#b2bec3"
: "transparent"};
flex-grow: 1;
transition: background-color 0.3s ease-in-out;
padding: 20px;
`;
- ref serves to use event on html tags like
focus
,blur
const inputRef = useRef<HTMLInputElement>(null);
const onClick = () => {
inputRef.current?.focus();
setTimeout(() => {
inputRef.current?.blur();
}, 5000);
};
..
<input ref={inputRef} placeholder="grab me" />
- string[] to ITodo[]
// src/atoms.tsx
export interface ITodo {
id: number;
text: string;
}
interface IToDoState {
[key: string]: ITodo[];
}
- substitute useRef with
useForm
- splice with ITodo
const taskObj = boardCopy[source.index];
boardCopy.splice(source.index, 1);
boardCopy.splice(destination?.index, 0, taskObj);
- handleSubmit
const onValid = ({ toDo }: IForm) => {
const newToDo = {
id: Date.now(),
text: toDo,
};
setToDos((allBoards) => {
return {
...allBoards,
[boardId]: [newToDo, ...allBoards[boardId]],
};
});
setValue("toDo", "");
};
mv src/apps/Trello/StudyDrag.tsx src/apps/Trello/Trello.tsx
- chore: update styled.form
- delete todo with drag
- persistant save
- create board
- DnD boards
- style setup
# src/App.tsx
body {
..
- background-color:${(props) => props.theme.bgColor};
+ background:linear-gradient(135deg,#e09,#d0e);
npm i framer-motion
- To animate, it should be
motion.{htmlTag}
import { motion } from "framer-motion";
..
<motion.div></motion.div>
- with styled component
- transition.type like curve
- initial to animate
// src/apps/Animation/animations/Spinner.tsx
const Box = styled(motion.div)`
width: 200px;
height: 200px;
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
export function Spinner() {
return (
<Wrapper>
<Box
transition={{ type: "spring", delay: 0.5 }}
initial={{ scale: 0 }}
animate={{ scale: 1, rotateZ: 360 }}
/>
</Wrapper>
);
}
- initial == start
- animate == end
const myVars = {
start: { scale: 0 },
end: { scale: 1, rotateZ: 360, transition: { type: "spring", delay: 0.5 } },
};
..
<Box variants={myVars} initial="start" animate="end" />;
- split animations with routes
- delayChildren: start all children with delay
- staggerChildren: give interval to each children to work consequently
// src/apps/Animation/animations/IphoneCamera.tsx
const boxVariants = {
start: {
opacity: 0,
scale: 0.5,
},
end: {
scale: 1,
opacity: 1,
transition: {
type: "spring",
duration: 0.5,
bounce: 0.5,
delayChildren: 0.5,
staggerChildren: 0.2,
},
},
};
// src/apps/Animation/animations/Gesture.tsx
const boxVariants = {
hover: { scale: 1.5, rotateZ: 90 },
click: { scale: 1, borderRadius: "100px" },
drag: { backgroundColor: "rgb(46, 204, 113)", transition: { duration: 10 } },
};
..
<Box
drag
variants={boxVariants}
whileHover="hover"
whileDrag="drag"
whileTap="click"
/>
- give dragConstraints with ref
- dragElastic allows over than constraints
const biggerBoxRef = useRef<HTMLDivElement>(null);
return (
<Wrapper>
<BiggerBox ref={biggerBoxRef}>
<Box
drag
dragSnapToOrigin
dragElastic={0.1}
dragConstraints={biggerBoxRef}
variants={boxVariants}
whileHover="hover"
whileTap="click"
/>
</BiggerBox>
..
- motion value does not trigger react render cycle: it's not react state
// src/apps/Animation/animations/MotionValue.tsx
const x = useMotionValue(0);
useEffect(() => {
x.onChange((latest) => {
console.log(latest);
});
}, [x]);
return (
<Wrapper>
<Box style={{ x }} drag="x" dragSnapToOrigin />
</Wrapper>
);
- useTransform animates scale
// src/apps/Animation/animations/MotionValue.tsx
const scale = useTransform(x, [-800, 0, 800], [2, 1, 0.1]);
..
<Box style={{ x, scale }} drag="x" dragSnapToOrigin />
- rotateZ roll the component while scrolling x
- useViewportScroll().scroll: gives absolute value, scrollYprogress gives interpolated value from 0 to 1
// src/apps/Animation/animations/MotionValue.tsx
const x = useMotionValue(0);
const rotateZ = useTransform(x, [-800, 800], [-360, 360]);
const gradient = useTransform(
x,
[-800, 800],
[
"linear-gradient(135deg, rgb(0, 210, 238), rgb(0, 83, 238))",
"linear-gradient(135deg, rgb(0, 238, 155), rgb(238, 178, 0))",
]
);
const { scrollYProgress } = useViewportScroll();
const scale = useTransform(scrollYProgress, [0, 1], [1, 5]);
..
<Wrapper style={{ background: gradient }}>
<Box style={{ x, rotateZ, scale }} drag="x" dragSnapToOrigin />
</Wrapper>
- pathLength draws svg from 0 to 1
// src/apps/Animation/animations/SVG.tsx
const svg = {
start: { pathLength: 0, fill: "rgba(255, 255, 255, 0)" },
end: {
fill: "rgba(255, 255, 255, 1)",
pathLength: 1,
},
};
..
<Svg
focusable="false"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
>
<motion.path
variants={svg}
initial="start"
animate="end"
transition={{
default: { duration: 5 },
fill: { duration: 1, delay: 3 },
}}
..
- AnimatePresence allows components to animate out when they're removed from the React tree.
const boxVariants = {
initial: {
opacity: 0,
scale: 0,
},
visible: {
opacity: 1,
scale: 1,
rotateZ: 360,
},
leaving: {
opacity: 0,
scale: 0,
y: 50,
},
};
..
<AnimatePresence>
{showing ? (
<Box
variants={boxVariants}
initial="initial"
animate="visible"
exit="leaving"
/>
) : null}
</AnimatePresence>
- slideX from +500 to -500
- custom sends data to variant which allows to use callback
- should set on both AnimatePresence and child component
AnimatePresence.mode="wait"
waits till prev component completely exits
const box = {
entry: (back: boolean) => ({
x: 500 * (back ? -1 : 1),
opacity: 0,
scale: 0,
}),
center: {
x: 0,
opacity: 1,
scale: 1,
transition: {
duration: 0.3,
},
},
exit: (back: boolean) => ({
x: 500 * (back ? 1 : -1),
opacity: 0,
scale: 0,
transition: { duration: 0.3 },
}),
};
..
const [visible, setVisible] = useState(1);
const [back, setBack] = useState(false);
const nextPlease = () =>
setVisible((prev) => {
setBack(false);
return prev === 10 ? 10 : prev + 1;
});
const prevPlease = () =>
setVisible((prev) => {
setBack(true);
return prev === 1 ? 1 : prev - 1;
});
..
<AnimatePresence custom={back} mode="wait">
<Box
custom={back}
variants={box}
initial="entry"
animate="center"
exit="exit"
key={visible}
>
{visible}
</Box>
</AnimatePresence>