Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

**Feature:** Splash hooks & useInterval #940

Merged
merged 22 commits into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 71 additions & 56 deletions src/Splash/Splash.Animation.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from "react"

import React, { useState } from "react"
import useInterval from "../useInterval"
import styled from "../utils/styled"

export interface Props {
isFullscreen?: boolean
size?: number
}

Expand Down Expand Up @@ -34,73 +35,87 @@ const bounce = (coord: number): number => {
return coord
}

const Container = styled("div")<{ size: number }>(({ size }: { size: number }) => ({
width: size,
height: size,
// css Hack so we dont need to worry about max(window.height,window.width)- Only needed when fullscreen is enabled
// https://spin.atomicobject.com/2015/07/14/css-responsive-square/
const FullScreenWrap = styled("div")({
position: "absolute",
width: "100%",
":after": {
content: "''",
display: "block",
paddingBottom: "100%",
TejasQ marked this conversation as resolved.
Show resolved Hide resolved
},
})

const Container = styled("div")({
position: "absolute",
top: "50%",
left: "50%",
transform: "translate3d(-50%, -50%, 0)",
}))
width: "100%",
TejasQ marked this conversation as resolved.
Show resolved Hide resolved
height: "100%",
})

const Box = styled("div")<{ x: number; y: number }>(({ x, y }) => ({
/// Move highly highly dynamic style out of css-js to prevent uneeded classname generation
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
const Box = styled("div")({
position: "absolute",
transition: "all 0.5s ease-in-out",
top: `calc(${(x / (squares - 1)) * 100}% + 4px)`,
left: `calc(${(y / (squares - 1)) * 100}% + 4px)`,
borderRadius: 6,
width: `calc(${100 / (squares - 1)}% - 8px)`,
height: `calc(${100 / (squares - 1)}% - 8px)`,
backgroundColor: "rgba(255, 255, 255, 0.06)",
}))

class Animation extends React.Component<Props, State> {
public state = {
animationStep: 0,
coordinates: Array.apply(null, { length: boxes })
.map(Number.call, Number)
.map(() => ({ x: integerRandom(squares), y: integerRandom(squares) })),
}

public animationInterval?: number
})

const initialState = {
animationStep: 0,
coordinates: Array.from(Array(boxes), (_, index) => index).map(() => ({
x: integerRandom(squares),
y: integerRandom(squares),
})),
}

// Shift the coordinate of every third tile in a random direction.
// Each animation shifts a different set of tiles.
public shiftSomeTiles() {
this.setState(prevState => ({
animationStep: prevState.animationStep + 1,
coordinates: prevState.coordinates.map((coord: { x: number; y: number }, index: number) => {
if (index % 3 === prevState.animationStep % 3) {
const dx = integerRandom(3) - 1
const dy = integerRandom(3) - 1
return {
x: bounce(coord.x + dx),
y: bounce(coord.y - dy),
const Animation: React.FC<Props> = ({ isFullscreen, size = 600 }) => {
const [state, updateAnimation] = useState<State>(initialState)

useInterval(
() => {
updateAnimation({
animationStep: state.animationStep + 1,
coordinates: state.coordinates.map((coord: { x: number; y: number }, index: number) => {
TejasQ marked this conversation as resolved.
Show resolved Hide resolved
if (index % 3 === state.animationStep % 3) {
const dx = integerRandom(3) - 1
const dy = integerRandom(3) - 1
return {
x: bounce(coord.x + dx),
y: bounce(coord.y - dy),
}
}
}
return coord
}),
}))
}

public componentDidMount() {
this.animationInterval = window.setInterval(this.shiftSomeTiles.bind(this), 5000)
}

public componentWillUnmount() {
clearInterval(this.animationInterval)
}

public render() {
const size = this.props.size || 600
return (
<Container size={size}>
{this.state.coordinates.map((coord: { x: number; y: number }, index: number) => (
<Box key={index} x={coord.x} y={coord.y} />
))}
</Container>
)
}
return coord
}),
})
},
5000,
true,
)

const children = state.coordinates.map((coord: { x: number; y: number }, index: number) => (
<Box
key={index}
style={{
top: `${(coord.x / (squares - 1)) * 100}%`,
left: `${(coord.y / (squares - 1)) * 100}%`,
}}
/>
))

// Only will change if isFullscreen or size changes, a workaround from not having to set outer container width and height to max(window.height, window.width)
return isFullscreen ? (
<FullScreenWrap>
<Container>{children}</Container>
</FullScreenWrap>
) : (
<Container style={{ width: size, height: size }}>{children}</Container>
)
}

export default Animation
57 changes: 16 additions & 41 deletions src/Splash/Splash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,47 +89,22 @@ const Static = styled("div")`
}
`

class Splash extends React.Component<SplashProps, Readonly<State>> {
public readonly state = {
animationSize: Math.max(window.innerWidth, window.innerHeight),
}

public static defaultProps = {
color: "#fff",
}

public handleResize = () => {
this.setState({
animationSize: Math.max(window.innerWidth, window.innerHeight) as number,
})
}

public componentDidMount() {
window.addEventListener("resize", this.handleResize)
}

public componentWillUnmount() {
window.removeEventListener("resize", this.handleResize)
}

public render() {
const { logo, color } = this.props
return (
<Container color={color}>
<Animation size={this.state.animationSize} />
<Content color={color}>
<TitleBar>
<OperationalLogo size={110} logo={logo} />
<TitleBarContent>
<h1>{this.props.title}</h1>
<div>{this.props.actions}</div>
</TitleBarContent>
</TitleBar>
<Static>{this.props.children}</Static>
</Content>
</Container>
)
}
const Splash: React.FC<SplashProps> = ({ color, logo, children, title, actions }) => {
return (
<Container color={color}>
<Animation isFullscreen />
<Content color={color}>
<TitleBar>
<OperationalLogo size={110} logo={logo} />
<TitleBarContent>
<h1>{title}</h1>
<div>{actions}</div>
</TitleBarContent>
</TitleBar>
<Static>{children}</Static>
</Content>
</Container>
)
}

export default Splash
113 changes: 113 additions & 0 deletions src/useInterval/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
An implementation of `setInterval` with Hooks.

### Reference

[Making setInterval Declarative with React Hooks](https://overreacted.io/making-setinterval-declarative-with-react-hooks/).

### Usage

#### Basic Counter

```jsx
function BasicCounter() {
const [count, setCount] = React.useState(0)

useInterval(() => {
setCount(count + 1)
}, 1000)

return <h1>{count}</h1>
}

;<BasicCounter />
```

#### Using two intervals to manipulate the counter

Example, we can have a delay of one interval be controlled by another:

```jsx
function Counter() {
const [start, setStart] = React.useState(false)
const [delay, setDelay] = React.useState(1000)
const [count, setCount] = React.useState(0)

// Increment the counter.
useInterval(() => {
if (start) {
setCount(count + 1)
}
}, delay)

// Make it faster every second!
useInterval(() => {
if (delay > 10 && start) {
setDelay(delay / 2)
}
}, 1000)

function handleReset() {
setDelay(1000)
}

return (
<>
<button onClick={() => setStart(!start)}>Toggle Counter!</button>
<h1>Counter: {count}</h1>
<h4>Delay: {delay}</h4>
<button onClick={handleReset}>Reset delay</button>
</>
)
}

;<Counter />
```

#### Immediate argument

If you need your callback to be delayed for an extended period of time, but you want it to trigger immediately upon mounting, then pass in immediate as the third argument.

```jsx
function MyComp() {
const [inverted, setInvert] = React.useState(false)
const [inverted2, setInvert2] = React.useState(false)

// Invert colors Immediately
useInterval(
() => {
setInvert(!inverted)
},
10000,
true,
)

// Invert colors
useInterval(() => {
setInvert2(!inverted2)
}, 10000)

return (
<div>
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
<div
style={{
background: inverted ? "navy" : "white",
color: inverted ? "white" : "navy",
}}
>
<h3>This immediately gets inverted with a navy background</h3>
</div>

<div
style={{
background: inverted2 ? "navy" : "white",
color: inverted2 ? "white" : "navy",
}}
>
<h3>This needs to wait for the delay to pass to fire</h3>
</div>
</div>
)
}

;<MyComp />
```
41 changes: 41 additions & 0 deletions src/useInterval/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import noop from "lodash/noop"
import { useEffect, useRef } from "react"
Copy link
Contributor

@stereobooster stereobooster Feb 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplest implementation would be

import { useEffect } from 'react';

const useInterval = (fn: () => void, delay: number) => {
  useEffect(() => {
    const id = setInterval(fn, delay)
    return () => clearInterval(id)
  })
};

export default useInterval;

UPD code works only because useEffect will be triggered on each render (which is wrong)

  useEffect(() => {
    const id = setInterval(fn, delay)
    return () => clearInterval(id)
-  })
+  }, [delay])

if we would add delay, it would brake. Example of the bug

const useInterval = (fn: () => void, delay: number) => {
  useEffect(() => {
    const id = setInterval(() => {
      console.log(1);
      fn();
    }, delay)
    return () => clearInterval(id)
  }, [delay])
};

function Counter() {
  const [count, setCount] = useState(0);
  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);
  return <h1>{count}</h1>;
}

count will change from 0 to 1 only once (because count is locally bound to function passed to useInterval), but console.log(1); will be called many times

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stereobooster ,Dan wrote a nice piece on this.

Simple Implementation works for simple cases ( and how its currently being used in Splash)

But this version is more scalable/reusable ...

"But setInterval() does not “forget”. It will forever reference the old props and state until you replace it — which you can’t do without resetting the time."

Where useRef comes into play...

"The problem boils down to this:

  • We do setInterval(callback1, delay) with callback1 from first render.
  • We have callback2 from next render that closes over fresh props and state.
  • But we can’t replace an already existing interval without resetting the time!

So what if we didn’t replace the interval at all, and instead introduced a mutable savedCallback variable pointing to the latest interval callback?"

"This mutable savedCallback needs to “persist” across the re-renders. So it can’t be a regular variable. We want something more like an instance field."

For Example, this allows us to change the delay or callback between renders without resetting the time

But if you want be to change to simple version, i will do that!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that the code should be clear, so people after us would be able to read it easily and change it if they want to. It takes quite some iterations to understand what happens here. After reading Dan's article this all start to make sense, but before it seemed over-engineered. So adding link to the Dan's article and maybe some more comments would help

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. @JoshRosenstein please add some documentation if you wish to keep the component the way it is. ❤️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoshRosenstein have you seen these comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mpotomin - It was my understanding the "simplest implementation" version breaks, and should keep the current useInterval as is, and i added additional comments since, Did i miss something ? Or just forgot to resolve?

Edit useInterval Test

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just forgot to acknowledge. Thanks for responding, @JoshRosenstein! @stereobooster is that okay with you?


/**
* Hook version of setInterval.
* @param {() => void} callback - Callback to be repeatedly excuted over a certain interval.
* @param {number | null} delay - Interval duration in ms. Pass null to cancel an execution.
* @param {boolean} immediate - Pass true to execute the callback immediately upon mounting.
* @see https://overreacted.io/making-setinterval-declarative-with-react-hooks/
*/
TejasQ marked this conversation as resolved.
Show resolved Hide resolved
export function useInterval(callback: () => void, delay: number | null, immediate?: boolean) {
TejasQ marked this conversation as resolved.
Show resolved Hide resolved
const savedCallback = useRef(noop)
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved

// Remember the latest callback.
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
// useEffect has no second argument so it will be executed after each render
// but we don't want to change this value directly in the body of the render function,
// because render should be pure function

// After every render, save the latest callback into our ref.
useEffect(() => {
savedCallback.current = callback
})

useEffect(() => {
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
if (immediate && delay !== null) {
savedCallback.current()
}
}, [immediate]) // when immediate changes, we want to restart the timer

// Set up the interval.
useEffect(() => {
if (delay === null) {
return undefined
}
// we can read and call latest callback from inside our interval:
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay]) // when delay changes, we want to restart the timer
}

export default useInterval
Loading