Skip to main content

16 posts tagged with "JavaScript"

View All Tags

class based React.js hooks

REF: [1]

Reactで複雑な処理を加えたとき、useMemo, useCallbackだらけになったり、 useRef, xxxRef.currentだらけになり、読みにくいコードになることがあります。 クラスベースで、Ctrlクラスを作成し、hookから操作します。 例として、useDelay.tsを定義します。 このフックは、実行を少し遅らせることで、重い処理などが重複するのを防ぎます。

import React from 'react'

class Ctrl {
listener = () => {};
callback = () => {};
timeStamp = 1000;

apply(callback = () => {}, timeStamp = 1000) {
this.callback = callback;
this.timeStamp = timeStamp;
return this.delay.bind(this);
}

delay(...args) {
const timeout = window.setTimeout(this.callback, this.timeStamp, ...args);
this.listener();
this.listener = () => void window.clearTimeout(timeout);
}
}

export default function $(callback = () => {}, timeStamp = 1000) {
const [ctrl] = React.useState(() => new Ctrl());
React.useEffect(() => ctrl.listener, [ctrl]);
return ctrl.apply(callback, timeStamp);
}

applyはstateが変化するごとにが実行され、状態や返す関数を更新しています。 delayは実行毎にlistenerをリセットし、時間がたつとcallbackを実行します。 次のアプリでは、文字の打ち込みが終わった1秒ごとに、callbackが動作します。 codesandbox でも試すことができます。

import React from "react";
import ReactDOM from "react-dom";
import $ from './useDelay';

function App() {
const [state, set] = React.useState`⚡Lets write here⚡`;
return (
<>
{ state }
<input
onKeyDown={(e) => set`🐛Now writing🐛`}
onKeyUp={$((e) => set`✅Completed✅`, 1000)}
/>
</>
);
}

ReactDOM.render(<App />, document.getElementById`root`);

ProjectedMaterial

REF: [[1], [2], [3], [4]]

Three.jsで、指定したカメラから見た光景を、TextureにするMaterialです。 また、ProjectedMaterialでは、envMapなども指定できたりします。 一つ注意なのが、project()を一度実行する必要があります。

import React from 'react';
import { extend } from '@react-three/fiber'
import ProjectedMaterial from "three-projected-material";

extend({ ProjectedMaterial });

function Mesh () {
const ref = React.useRef()
React.useEffect(() => void ref.current.material.project?.(ref.current), []);
return (
<mesh ref={ref}>
<projectedMaterial args={[{texture, camera}]} />
<boxGeometry args={[1, 1, 1]} />
</mesh>
)
}

web midi api

MIDIAccessについて

まず、flagsからWebMIDIAPIを有効にし、browserを再起動します。 次に、サーバーサイドや非対応の対策nativeRmaを用意します。

const options = { sysex: true, software: true }
const supported =
typeof navigator !== 'undefined' && // @ts-ignore
typeof navigator.requestMIDIAccess === 'function'
const nativeRma = () => {
if (!supported) console.warn('Cannot supported Web MIDI API') // @ts-ignore
else return navigator.requestMIDIAccess(options)
}

次のようにするとIllegal invocationエラーが起きます。 (ネイティブのコードなので、他に代入できません。)

nativeRma = navigator.requestMMIDIAccess
// TypeError: Failed to execute 'requestMIDIAccess' on 'Navigator': Illegal invocation

Promiseで取得できるMIDIAccessから、各MIDIPortを操作できます。

const change = (access: MIDIAccess) => {}
const error = console.error
nativeRma()?.then(change, error)

onstatechangeについて

change内でMIDIAccess.onstatechangeを指定すると、 portが変化するごとに実行してくれます。 onstatechangeMIDIPortからも指定できます。

MIDIAccess {
onstatechange: (e: MIDIConnectionEvent) => void
inputs: maplike <DOMString, MIDIInput>;
outputs: maplike <DOMString, MIDIInput>;
sysexEnabled: boolean
}

MIDIPort {
onstatechange: (e: MIDIConnectionEvent) => void
Promise<MIDIPort> open
Promize<MIDIPort> close
}

maplikeなので、clear(), delete(), set()が使用できません。 maplikeのkey名が長く、get(), has()も使いにくいです。 ですので、size, keys(), values(), entries(), forEach()を主に使います。

onmidimessage, sendについて

MIDIInput.onmidimessageからデータを受け取り、MIDIOutput.sendからデータを送信します。 両方ともMIDIPortを継承しているため、onstatechangeを指定することもできます。

MIDIInput extends MIDIPort {
onmidimessage: (e: MIDIMessageEvent) => void
}

MIDIOutput extends MIDIPort {
send (data, timestamp) => void
clear () => void
}

Eventについて

REF

onstatechangeの引数のMIDIConnectionEventと, onmidimessageの引数のMIDIMessageEventEventから継承されているので、 addEventListennerで関数を登録したり timeStampで時間を計測したりできます。

interface MIDIConnectionEvent extends Event {
target: MIDIAccess
port: MIDIPort
}

interface MIDIMessageEvent extends Event {
data: Unit8Array
}

random color

REF : [1, 2, 3]

stackoverflowのコードを jsbench.meでベンチマークした結果です。 (markdownの関係で、\| となっていますが、正しくは |です)

.toStringScore
'#' + (Math.random() * 0xffffff \| 0).toString(16)Fastest💪
'#' + (Math.random() * (1<<24) \| 0).toString(16)Fastest💪
'#' + (Math.random() * (1<<24) << 0).toString(16)+1.24%👍
'#' + parseInt(Math.random() * 0xffffff).toString(16)+2.25%👍
'#' + Math.floor(Math.random() * (1<<24)).toString(16)Fastest💪
'#' + Math.round(Math.random() * 0xffffff).toString(16)+7.51%👍
.sliceScore
'#' + Math.random().toString(16).slice(-6)+53.96%🐢
'#' + Math.random().toString(16).slice(2,8)+ 52.54%🐢
'#' + (Math.random().toString(16)+'00000').slice(2,8)+67.83%🐢
.substrScore
'#' + Math.random().toString(16).substring(-6)+50.28%🐢
'#' + (Math.random().toString(16) + "000000").substring(2,8)+67.44%🐢
'#' + ((1<<24)*(Math.random()+1) \| 0).toString(16).substr(1)+15.7%👍
'#' + (((1+Math.random())*(1<<24) \| 0).toString(16)).substr(-6)+15.91%👍
'#' + (0x1000000+Math.random()*0xffffff).toString(16).substr(1,6)+66.46%🐢
otherScore
'#' + Math.round((0x1000000 + 0xffffff * Math.random())).toString(16).slice(1)+20.36%👍
"#xxxxxx".replace(/x/g, y=>(Math.random()*16\|0).toString(16))+85.52%🐢
rgb(${[1,2,3].map(x => Math.random()*256\|0)})+69.62%🐢
hsla(${Math.random() * 360}, 100%, 50%, 1)+ 57.79%🐢
["r", "g", "b"].reduce(r => r + ("0" + ~~(Math.random()*256).toString(16)).slice(-2), "#")+89.79%🐢

range function in JavaScript

lodashを参考に,よく使うrangeをより早く実装してみました。

function range (n=0) {
const ret = new Array(n)
for (;n--;) ret[n] = n
return ret
}

いくつかのショートハンドもありますが、 keys()を使うと90%近くも遅くなるらしいです。 また、nを大きくしたところ、map()でも90%ほど遅くなりました。

range(Math.random() * 100 << 0) // Fastest 💪
keys(Math.random() * 100 << 0) // 93% slower 🐢
map(Math.random() * 100 << 0) // 60% slower 🐢
function keys (n=0) {
return [...Array(n).keys()]
}

function map(n=0) {
return [...Array(n)].map((_, i) => i)
}

JavaScript tips

Shorthand

Boolean(0)!!0
String(3.14)3.14 + ""
Number("3.14")+ "3.14"
Math.floor(3.14)~~3.14 or
3.14 << 0
undefinedvoid 0
100001e4

flatMap

result = []
for (v of array)
if (v !== 0)
array.push(1/v)
  • array.map(v => v !== 0 && 1/v).filter(v => v)
  • array.flatMap(v => v === 0? []: 1/v)

Math

const {sin, asin} = Math
[0, 1, 2].map(sin).map(asin)

template literal

function upper(args) {
return args[0].toUpperCase()
}
const text = upper`hi` // HI

functional object

const hello = () => 'HELLO'
hello.world = () => 'WORLD'
hello() // HELLO
hello.world() // WORLD

assignment

let t = -1,
dt = 100 - (~t? t: (t = 0))
console.log(t) // 0

django manage SPA

アプリを作成する

djangoのstartappの代わりに、create-react-appでアプリを作成します。 nodeをインストール後、npm install -g create-react-appを実行することで使用できます。

  • django-admin startproject my-project
  • cd my-project
  • create-react-app my-react-app
  • cd my-react-app

ルーティングの設定

urls.pyを次のように設定させます

from django.conf import settings
...
url_patterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

submodules = ["my-react-app"]

for name in submodules:
route = name if name != "core" else ""
temp = 'static/%s/index.html' % name
view = TemplateView.as_view(template_name=temp)
urlpatterns += [re_path(route, view, name=name)]

staticのディレクトリを設定する

ビルド結果のフォルダを次のようにsettings.pyで指定させます。

submodules = ["my-react-app"]
STATICFILES_DIRS = []
for s in submodules:
STATICFILES_DIRS += [
os.path.join(BASE_DIR, "%s/build/%s/static" % (s, s)),
os.path.join(BASE_DIR, "%s/build" % (s))
]

ここで二重に設定させたのは、複数のReactアプリが存在する場合、 static内のファイルを同じ場所で管理できるようにするためです。 このとき、staticディレクトリが重複してしまうため、 ignoreを指定してファイルを集める必要があります。

CRAのbuild先を上書きする

npm install -D react-app-rewiredを実行後、config-overrides.jsを次のようにします

const fs = require('fs');
const path = require('path');
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

module.exports = {
paths: (paths, env) => {
const basename = path.basename(__dirname)
paths.appBuild = resolveApp(`build/${basename}`);
return paths;
}
}
  • npx react-app-rewired build
  • python manage.py collectstatic --ignore=static/

Rest APIを作る

先ほど作成したmy-react-appでAPIを使用します。 python manage.py startapp my-react-appを実行すると、 ディレクトリ内にPythonファイルが作成されます。 settings.pyを編集します。

INSTALLED_APPS += ['my-react-app']

my-project/urls.pyに次のように追加します

from django.conf import settings
# ...
url_patterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

submodules = ["my-react-app"]

for name in submodules:
route = name if name != "core" else ""
temp = 'static/%s/index.html' % name
view = TemplateView.as_view(template_name=temp)
urlpatterns += [re_path(route, view, name=name)]

開発用にサーバーを立てるコマンドを作成する

DjangoとNode.jsのサーバーを同時に用意する際のコマンドを用意しておきます。

my-react-app/management
│ __init__.py

└─commands
│ npm_start_runserver.py
└─ __init__.py

npm_start_runserver.pyを次のように編集します。

import sys
import subprocess
from django.core.management.base import BaseCommand

class Command(BaseCommand):
help = "npm start and runserver"

def handle(self, *args, **options):
proc = subprocess.Popen(["npm", "start"], shell=True, cwd='./note')
cmds = [sys.executable, sys.argv[0], 'runserver', '0.0.0.0:8000']
subprocess.run(cmds, shell=True, cwd=".")
proc.close()

python manage.py npm_start_runserver で立ち上げることができます。

Typescript | and &

REF

Typescriptの合併|と公差&がわかりにくかったので、食事の例で考えてみました。 献立表を作るとき、次のようにサラダとパスタとピザの型を定義します。 今回は、サラダとパスタのトマトは数を数えられるとしてnumber型、 ピザのトマトはペースト状なのでboolean型にしているので注意してください。

type Salad = {tomato: number}
type Pasta = {tomato: number, macaroni: boolean}
type Pizza = {tomato: boolean, cheeze: boolean}

合併|について

各食事で必ずサラダをとり、ランチではパスタ、ディナーではピザを食べる ときの、 サラダとPasta,Pizzaの合併|を考えます。

type Lunch1  = Salad | Pasta
type Dinner1 = Salad | Pizza

合併は、いわゆる"または"の意味なので、どちらかの食事である必要があります。 ランチはPasta ⇒ Saladの関係なので、 少なくともtomatoがbooleanであればtrueとなり、 ディナーはPizzaとSaladは全く異なるので、SaladでもPizzaでもないとfalseとエラーになります。

const  salad: Lunch1  = {tomato:0} //OK!
const pasta: Lunch1 = {tomato:0, macaroni:true} //OK!
const pizza: Dinner1 = {tomato:false, cheeze:true} //OK!
const _pizza: Dinner1 = {tomato:1 , cheeze:true} //ERROR!
//Object literal may only specify known properties, and 'cheeze' does not exist in type 'Salad'.

交差&について

ランチではパスタ、ディナーではピザを食べる食生活として、サラダとの交差&を用いて定義します。

type Lunch2  = Salad & Pasta
type Dinner2 = Salad & Pizza

公差&はかつという意味なのですが、Typescriptでは少しあつかいにくいので注意が必要です。 ランチではPasta ⇒ Saladの関係であり、必要条件であるパスタでないとfalseになるので、 SaladかつPastaだとマカロニサラダしか該当しなくなります。 ディナーに関しては、tomatoの型がSaladとPizzaで異なるので、 tomato:number&boolean)=>tomato:neverになり, どんな料理でも合致しなくなります。

const salad2: Lunch2 = {tomato:0} //ERROR!
const pasta2: Lunch2 = {tomato:0, macaroni:true} //OK!

//Property 'macaroni' is missing in type '{ tomato: number; }' but required in type 'Pasta'.

const _pizza2: Dinner2 = {tomato:1 , cheeze:true} //ERROR!
const __pizza2: Dinner2 = {tomato:true, cheeze:true} //ERROR!

// Type 'number' is not assignable to type 'never'.
// Type 'true' is not assignable to type 'never'.

型のmergeについて

重複したkeyの型が異なるSaladとPizzaの型を合体させるには、条件とマップが必要となります。 Pizzaのkeyに対して、keyがSaladのkeyにあればSaladの型、出なければPizzaの型を返すことができます。 このとき、tomatoの型は、saladが優先されるのでnumber型になります。

type Salad = {tomato:number}
type Pizza = {tomato:boolean, cheeze:boolean}
type Dinner = {
[K in keyof Pizza]: K extends keyof Salad ? Salad[K] : Pizza[K]
} & Salad
const pizza: Dinner = {tomato:0 , cheeze:true} // OK !
const _pizza: Dinner = {tomato:false, cheeze:true} //Error !

// Type 'false' is not assignable to type 'number'.
// Dinner is {tomato:number, cheeze:boolean}

JS, TS, Error and solution

よく指摘されるエラーと、回避させる方法をまとめました。

Uncaught SyntaxError: Unexpected token '.'

  • xxx.yyyなどで参照するとき、xxxがnullだとerrorが出ます。
  • xxx && xxx.yyyと一間開けるか、xxx?.yyyで回避できます。

Uncaught TypeError: xxx.map is not a function.

  • props.children.map(v=>v.key)などで参照するとき、childrenが配列でないとerrorが出ます
  • const getarr =a=>a instanceof Array?a:a?[]:[a]が便利。
  • getarr(props.children).map(v=>v.key)と一間開けるか、React.Children.mapを使います。

Cannot read property '1' of undefined

  • 長さ1の配列arrarr[1]するとプロパティーがないといわれます。
  • arr.find((_,i) = >i===1) || null を使うか、型を定義します。

Typescript error

JSX element 'T' has no corresponding closing tag.

  • Typescriptの<T>がcomponentsとして認識されてしまいます。
  • tsconfigを直すか、<T><T=unknown>にします。

Argument of type 'any[]' is not assignable to parameter of type 'ConcatArray<never>'.

  • 型がない場合は、.concat(...(arr as never[])を通します。

React hooks

Reactは簡単にいうと、Webなどの処理と開発を最適化するための新しいエコシステムといえます。 また、hookは関数ベースのみでReactを実装する方法なので、型システムと相性がいいです。

前半では、DOMを直接触らない大体のjsコードをhookで使用する方法をまとめました。 後半では、前半で使ったhookを使って、独自のhookを新たに作る方法をまとめました。

useRefについて

Reactでは、データは一方向(親から子)へ渡されて計算されるが、 親から子のElementのrefに参照することで, 子の要素の値を外から参照したり操作できます。 また、React向けでないライブラリの変数を入れることで、全体が再renderしても初期化されないようにできます。

以下の例で➊では、指定した要素の値を参照するための基本的な使い方です。 ➋では、再renderしたときuseCallbackやChildrenのpropsを変化させず、子要素の再renderを防ぎます ➌では、App全体が再renderしたときに再びインスタンス化されないように値を保持します。

import {useRef, useCallback} from 'react'
const App = ({src="/static/test.png"}) => {
const ref = useRef(null) // ➊:通常のref
const err = useRef(false) // ➋:変化しても再renderしたくない!
const obj = useRef(new Image())// ➌:再renderしても初期化されない!
const onError = useCallback(()=>(err.current=true), [])
const onClick = useCallback(()=> err.current&&window.open(ref.current.src)),[])
return <img {...{src, ref, onClick, onLoad}} />
}

useEffectについて

componentを生成し、Renderしたあとに実行する処理を入れます。 例えば、fetchなど時間がかかる処理を入れることで、ほかの要素のrenderに影響を与えません。

また、第二引数を空の配列にすることで、再renderしたときに再実行したくない重い処理を入れることができます。 React向けではない(DOMを直接触るような)ライブラリは、すべての処理をこの中の入れることで利用できます。

const App = (props) => {
const [data, set] = useState('')
useEffect(()=>{
fetch(props.url).then(res => set(res))
}, [url])
return data && <span>{data}</span>
}

その他

  • useState:値が変化したら、再renderしてほしいような値に使います。(特に表示させる値)
  • useMemo : とりあえずすべての変数をこの中に入れておくと、高速化します。
  • useCallback: とりあえずすべての関数をこの中に入れておくと、高速化します。

自作hookについて

アプリの状態を保存するuseStateでは、前の値を参考に新たな値をsetするときは関数を引数に指定します。 例として、window.location から状態を管理するhookを作成します。 事前に、新しい値に関数を指定できるように、次のような型を定義します。

export type BasicProps<T>  = (()=>T) | T
export type BasicState<T> = ((pre:T)=>T) | T
export type BasicAction<T> = (fn:BasicState<T>) => void

引数の型が関数の場合を最初に除き、useRef内で値を補完することで、 (useStateのset(p=>p)の様な)、過去を参照するhookを作成することができます。 useRefにはデフォルト値と入力値をmergeして入力することで、多くの状態を同時に管理できます。

import {useState, useRef} from 'react'
import {Page, Conf} from '../types'
import {defaultConf, defaultPage} from '../utils'

export const usePage = <T=any>(
props :BasicProps<Partial<Page<T>>>={},
config:BasicProps<Partial<Conf<T>>>={},
) : [
Page<T>,
BasicAction<Partial<Page<T>>>
] => {
if (typeof props === "function")
props = props()
if (typeof config === "function")
config = config()
const pageRef = useRef({...defaultPage, ...props } as Page<T>)
const confRef = useRef({...defaultConf, ...config} as Conf<T>)
const [page, set] = useState(pageRef.current)

const setPage = useCallback(state => {
if (typeof state === "function")
state = state(pageRef.current as Partial<Page<T>>)
pageRef.current = {...pageRef.current, ...state}
set(pageRef.current)
}, [])

return [page, setPage]
}