Stickey Effects

CSS に `position: stickey` という指定があります。指定要素がスクロール位置まで到達した場合、要素を画面位置固定する style です。この stickey ですが、ブラウザサポート状況がイマイチです。また、ピタっと固定されたタイミングを、javascript で捉える方法がありません。本日はこの振る舞いを実装しつつ、スクロール位置に応じて外部通知する手法を紹介します。
code: [github](https://github.com/takefumi-yoshii/react-hooks-ogiri/tree/master/apps/stickey-effects) / `$ yarn 1209`

|![1209.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/ead1f21a-ca6f-73cf-a1b9-a95fe12269dc.jpeg)|![1209-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/0e03504c-914b-015e-0d80-5d918a9508df.jpeg)|![1209-2.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/dc661a3f-a31f-12a7-114b-062371f09404.jpeg)|
|---|---|---|

# Context API / Provider 構成

本日も Context API を利用します。現在表示エリアに入っている Section の index を current に保持します。まずは型付き空 context を生成します。

```components/context.ts
import { createContext, Dispatch, SetStateAction } from 'react'
export const CTX = createContext({} as {
  current: number
  setCurrent: Dispatch<SetStateAction<number>>
})
```

次に、useState の戻り値を Provider の初期値として注入します。先日と一緒です。

```components/provider.tsx
export default (props: Props) => {
  const [current, setCurrent] = useState(0)
  return (
    <Provider value={{ current, setCurrent }}>
      {props.children}
    </Provider>
  )
}
```
App index で Provider を最高階に設置します。count はダミー作成用なので、気にしないでください。

```components/index.tsx
const count = 5
export default () => (
  <Provider>
    <Sections count={count} />
    <Indicate count={count} />
  </Provider>
)
```

# useStickyWrapper

Custom Hooks は今までのサンプルとほとんど変わりありません。画面内判定ロジックが多少異なり、今回は画面上部より上に section 上辺が位置する場合「画面内」と判定しています。

```sections/section/useStickyWrapper.ts
const useStickyWrapper = (props: Props) => {
  const [state, setState] = useState<State>(...)
  const options = useMemo(...)
  useEffect(...) // 画面スクロール時処理
  useEffect(...) // 画面内判定処理
}
```

今回は wrapper コンポーネントと Custom Hooks を分離しています。

```components/sections/section/stickyWrapper.tsx
export default (props: Props) => {
  const ref = useRef({} as HTMLDivElement)
  useStickyWrapper({
    ref,
    onEnter: props.onEnter,
    onLeave: props.onLeave,
    throttleInterval: props.throttleInterval
  })
  return (
    <div
      className={props.className}
      id={props.id}
      ref={ref}
    >
      {props.children}
    </div>
  )
}
```

# Context の状態を変更する Section

ここまでで用意した StickyWrapper Component を利用し、Section を組み立てます。画面内外判定時の callback props は、親から注入されるハンドラをバインドします。

```components/sections/section/index.tsx
const View = (props: Props) => (
  <StickyWrapper
    id={`section${props.index}`}
    className={props.className}
    onEnter={props.onEnter}
    onLeave={props.onLeave}
  >
    ...
  </StickyWrapper>
)
```
以下が親 Container です。「onEnter」「onLeave」 で Context の current を更新しているのが分かります。同時に、自身が画面内に入っているか否かのフラグを useState で確保し、子コンポーネントに注入します。

```components/sections/section/index.tsx
export default (props: ContainerProps) => {
  const [isEnter, updateState] = useState(false)
  const { setCurrent } = useContext(CTX)
  const onEnter = useCallback(() => {
    updateState(true)
    setCurrent(props.index)
  }, [])
  const onLeave = useCallback(() => {
    updateState(false)
  }, [])
  return useMemo(
    () => (
      <StyledView
        index={props.index}
        title={props.title}
        onEnter={onEnter}
        onLeave={onLeave}
        isEnter={isEnter}
        isLast={props.isLast}
        isFirst={props.isFirst}
      />
    ),
    [isEnter]
  )
}
```


# Indicate

画面右側に表示されているカレント表示です。Context の状態を参照します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59071/982aa159-65c0-a9fd-6b71-220ee4d6e70d.png)

```components/indicate/item.tsx
export default (props: ContainerProps) => {
  const { current } = useContext(CTX)
  const isCurrent = useMemo(() => current === props.index, [
    current
  ])
  const style = useMemo(
    () =>
      isCurrent
        ? {
            transform: 'scale(1.5)',
            backgroundColor: styles.blue
          }
        : {
            transform: 'scale(1)'
          },
    [isCurrent]
  )
  return useMemo(
    () => (
      <StyledView index={props.index} style={style} />
    ),
    [style]
  )
}
```

ここも memoize をサボらず施します。視覚効果としては地味ですが、アイデア次第で色々な機能に応用できそうです。

```components/indicate/item.tsx
const StyledView = styled(View)`...`
```
```components/indicate/item.tsx
const View = (props: Props) => (
  <span className={props.className} style={props.style} />
)
```