#StackBounty: #javascript #performance #html #react.js #jsx Reduce multiple re rendering of components in React

Bounty: 50

Following is my working application that contains a Search Container, a Sorting Container and a listing Container.

Based on Search and Sort the listing data changes in the listing container.

The application works fine though I noticed in my Search container whenever I am changing the input value in an input box, select component is re rendering and vice versa(check console).

Is there any pattern or modification I can do to minimise the re renders in the application ?

Working Demo – https://ojlrd.csb.app/

Codesandbox – https://codesandbox.io/s/rerender-ojlrd?file=/src/index.js


Code –

Page.js

const Page = () => {
  const [pageData, setPageData] = useState({
    search: {},
    sort: {
      sortBy: ''
    }
  });

  const getData = useCallback((type, data) => {
    setPageData(prevVal => {
      return {
        ...prevVal,
        [type]: {...data}

      }
    })
  }, [setPageData]);
  

  return (
    <div className="container">
      <SearchContainer getData={getData} />
      <SortingContainer getData={getData} />
      <ListingContainer urlData={pageData} />
    </div>
  );
};

export default Page;

SearchContainer.js

const SearchContainer = ({
  getData = () => {}
}) => {
  const [searchData, setSearchData] = useState({
    search1: '',
    search2: '13',
    search3: '',
    search4: '44'
  });

  useEffect(() => {
    getData('search', searchData);
  }, [searchData, getData]);

  const eventHandler = e => {
    setSearchData(prevVal => {
      return {
        ...prevVal,
        [e.target.name]: e.target.value
      }
    })
  }

  return (
    <div className="container-search">
      <h3>Search container</h3>
      <div className="container-search-actions">
        <Input
          initialValue=''
          label='Input 1'
          name="search1"
          eventHandler={eventHandler}
        />
        <Select
          label='Select 2'
          name="search2"
          options={[
            {label: 'Label 11', value: '11'},
            {label: 'Label 12', value: '12'},
            {label: 'Label 13', value: '13'},
            {label: 'Label 14', value: '14'}
          ]}
          selectedValue='13'
          eventHandler={eventHandler}
        />
        <Input
          initialValue=''
          label='Input 3'
          name="search3"
          eventHandler={eventHandler}
        />
        <Select
          label='Select 4'
          name="search4"
          options={[
            {label: 'Label 41', value: '41'},
            {label: 'Label 42', value: '42'},
            {label: 'Label 43', value: '43'},
            {label: 'Label 44', value: '44'}
          ]}
          selectedValue='44'
          eventHandler={eventHandler}
        />
      </div>
    </div>
  )
}

export default SearchContainer;

SortingContainer.js

const SortingContainer = ({
  getData = () => {}
}) => {
  const [sortData, setSortData] = useState({
    sortBy: 'desc'
  })

  useEffect(() => {
    getData('sort', sortData);
  }, [sortData, getData]);

  const eventHandler = e => {
    setSortData(prevVal => {
      return {
        ...prevVal,
        [e.target.name]: e.target.value
      }
    })
  }


  return (
    <div className="container-sorting">
      <Select 
        label='Sort By'
        name='sortBy'
        options={[
          {label: 'Ascending', value: 'asc'},
          {label: 'Descending', value: 'desc'}
        ]}
        selectedValue='desc'
        eventHandler={eventHandler}
      />
    </div>
  );
}

export default SortingContainer;

ListingContainer.js

const ListingContainer = ({ urlData }) => {
  const createURL = (urlData) => {
    const { search, sort } = urlData;
    let url = "/api?";

    [search, sort].forEach((element) => {
      for (const [key, value] of Object.entries(element)) {
        url += `&${key}=${value}`;
      }
    });
    return url;
  };
  return (
    <div className="container-listing">
      This is a listing container
      <p>URL Formed - </p>
      <p className="bold">{createURL(urlData)}</p>
    </div>
  );
};

export default ListingContainer;


Get this bounty!!!

#StackBounty: #beginner #react.js #typescript Alarm Clock with React.js

Bounty: 100

Preamble

This is an Alarm Clock app. The way it works is pretty simple: you change the time by pressing the +/- buttons and set the time by pressing the play button. When the time is up, the alarm will ring at which point you can stop it by pressing the pause button. This question is already long as it is so I won’t be including everything required to run the application yourself. But if you’d like to do that, the project is hosted on Github and can be found here. A live version of the app is also available and can be found here.

Comments

In the the beginning, from thinking about how the application would look like, I identified five components:

  • a ChangeTimeButton component which would be the +/- buttons for changing the hour and minute;
  • an ArmButton component to set the alarm to ring at the specified time;
  • a Controls component that would contain the ArmButton and ChangeTimeButtons;
  • a Clock component which would simply display the time;
  • an App component that would contain the Clock and Controls components.

Components

After working with this structure for some time, I felt that ChangeTimeButton had become too complicated. It needed to be a button, which means it had to trigger some action after it was pressed; furthermore, it should be possible for the user to hold it in order to change the hour or minute continuously, and it’s appearance while being pressed should be different; it would need to handle both mouse and touch events; a sound should play when it was pressed, and so on. All of this, I thought, had more to do with the fact that this was a button (even if it was a specific type of button) than the fact that it would be used to change the time. So I refactored ChangeTimeButton into two components: one concerned with changing the time and another concerned with being a button. I also broke up ArmButton in a similar way.

Something else that made ChangeTimeButton complicated was that, after the alarm was armed, I did not want the user to still be able to change the time, so there would have to be some notion of ChangeTimeButton being off, which would mean that the user would not be able to interact with it, and that it’s appearance would need to change in order to convey that idea. Later, I also found out that I needed to prevent more than one ChangeTimeButton from being pressed at the same time, so it was necessary to maintain state about which one was currently being pressed and disallow all others from modifying the time. But this did not require a change in appearance, so I decided I needed to separate the notion of ChangeTimeButton being off from the notion of it being simply disabled.

Another problem I had was that, because the ChangeTimeButtons would be positioned on each side of the ArmButton, I couldn’t think of a good way to make a component that could contain all of them while maintaining state about which one was being pressed. The solution I came up with was to use a custom hook that internally calls setState on all ChangeTimeButtons with some global data. This allowed me to access and modify state shared by all instances of ChangeTimeButton without the need for a container component or a context.


Components

↓ App.tsx

import React, { useState, useRef, useCallback } from "react";
import Clock from "@components/Clock";
import Controls from "@components/Controls";
import useConstructor from "@hooks/useConstructor";
import HighResolutionTimer from "@src/HighResolutionTimer";
import { calcTimeUntilAlert, changeTime, getCurrentTime } from "@src/time";
import "./App.scss";

export default function App() {
    const [mode, setMode] = useState<types.AlarmClockMode>("idle");
    const [time, setTime] = useState<types.Time>();
    const timeoutId = useRef<number>();

    useConstructor(() => {
        const json = localStorage.getItem("time");

        let time;
        if (json === undefined) {
            time = getCurrentTime();
        } else {
            time = JSON.parse(json);
        }

        setTime(time);
    });

    const armButtonCallback = useCallback(() => {
        if (mode === "idle") {
            setMode("armed");

            let delta = calcTimeUntilAlert(time);
            timeoutId.current = window.setTimeout(() => {
                setMode("fired");
            }, delta);

            localStorage.setItem("time", JSON.stringify(time));
        } else {
            setMode("idle");
            clearTimeout(timeoutId.current);
        }
    }, [mode, time])

    const changeTimeButtonCallback = useCallback((type: types.ChangeTimeButtonType) => {
        const f = {
            "h+": (time: types.Time) => changeTime(time,  1,  0),
            "h-": (time: types.Time) => changeTime(time, -1,  0),
            "m+": (time: types.Time) => changeTime(time,  0,  1),
            "m-": (time: types.Time) => changeTime(time,  0, -1)
        }[type];
        setTime(time => f(time));
    }, []);

    return (
        <div className="outerContainer">
            <div className="innerContainer">
                <Clock time={time} />
                <Controls
                    mode={mode}
                    armButtonCallback={armButtonCallback}
                    changeTimeButtonCallback={changeTimeButtonCallback}
                />
            </div>
        </div>
    );
}

↓ Clock.tsx

import React from "react";
import { formatTime } from "@src/time";
import "./Clock.scss";

type PropsType = { time: types.Time };

export default function Clock(props: PropsType) {
    return (
        <div className="Clock">
            <span className="Clock_fg">{formatTime(props.time)}</span>
            <span className="Clock_bg">88:88</span>
        </div>
    );
};

↓ Controls.tsx

import React, { useEffect, useMemo } from "react";
import ArmButton from "@components/ArmButton";
import ChangeTimeButton from "@components/ChangeTimeButton";
import { useClasses, serializeClasses } from "./useClasses";
import "./Controls.scss";

type PropsType = {
    mode: types.AlarmClockMode;
    armButtonCallback: () => void;
    changeTimeButtonCallback: (type: types.ChangeTimeButtonType) => void;
};

export default function Controls(props: PropsType) {
    const {mode, armButtonCallback, changeTimeButtonCallback} = props;

    const [classes, setClasses] = useClasses();

    const isNotIdle = mode !== "idle";
    useEffect(() => setClasses({Controls__isNotIdle: isNotIdle}), [isNotIdle]);

    return (
        <div className={serializeClasses(classes)}>
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="h+"
                className="ChangeTimeButton__left"
            />
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="h-"
                className="ChangeTimeButton__left"
            />
            <ArmButton
                callback={armButtonCallback}
                mode={mode}
            />
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="m-"
                className="ChangeTimeButton__right"
            />
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="m+"
                className="ChangeTimeButton__right"
            />
        </div>
    );
}

↓ ChangeTimeButton.tsx

import React, { memo, useMemo, useCallback } from "react";
import HoldableButton from "@components/HoldableButton";
import { PlusIcon, MinusIcon } from "./icons";
import usePressed from "./usePressed";
import ChangeTimeButtonPressAndHoldSoundPath from "./ChangeTimeButtonPressAndHold.mp3";
import "./ChangeTimeButton.scss";

type PropsType = {
    callback: (type: types.ChangeTimeButtonType) => void;
    off: boolean;
    type: types.ChangeTimeButtonType;
    className: string;
};

const ChangeTimeButton = memo((props: PropsType) => {
    const { callback, type, off, className } = props;

    const [pressed, setPressed] = usePressed();
    const disabled = pressed !== null && pressed !== type;

    const onPress   = useCallback(() => { callback(type); setPressed(type) }, [type]);
    const onRelease = useCallback(() => setPressed(null), []);
    const onHold    = useCallback(() => callback(type), [type]);

    const icon = useMemo(() => {
        return (type === "h+" || type === "m+") ? <PlusIcon/> : <MinusIcon/>;
    }, []);

    return (
        <HoldableButton
            onPress={onPress}
            onRelease={onRelease}
            onHold={onHold}
            disabled={disabled}
            off={off}
            sound={ChangeTimeButtonPressAndHoldSoundPath}
            className={`ChangeTimeButton ${className}`}
        >
            {icon}
        </HoldableButton>
    );
});

export default ChangeTimeButton;

↓ HoldableButton.tsx

import React, { memo, useEffect, useRef } from "react";
import useConstructor from "@hooks/useConstructor";
import { useClasses, serializeClasses } from "./useClasses";
import HighResolutionTimer from "@src/HighResolutionTimer";
import AudioManager, { Sound } from "@src/AudioManager";

type PropsType = React.PropsWithChildren<{
    onPress:   Function;
    onRelease: Function;
    onHold:    Function;
    disabled:  boolean;
    off:       boolean;
    sound:     string;
    className: string;
}>;

const HoldableButton = memo((props: PropsType) => {
    const [classes, setClasses] = useClasses();
    useEffect(() => setClasses({HoldableButton__off: props.off}), [props.off]);

    const spanRef   = useRef<HTMLAnchorElement>();
    const timer     = useRef<HighResolutionTimer>();
    const sound     = useRef<Sound>();
    const isPressed = useRef(false);

    useConstructor(() => {
        timer.current = new HighResolutionTimer(110, 400);

        const audioManager = AudioManager.getInstance();
        sound.current = audioManager.createSound(props.sound);
    });

    const press = (e: any) => {
        e.preventDefault();

        if (e.type === "mousedown" && (("buttons" in e && e.buttons !== 1) || ("which" in e && e.which !== 1))) {
            return;
        }

        if (props.off || props.disabled) {
            return;
        }

        isPressed.current = true;
        setClasses("HoldableButton__pressed");

        props.onPress();
        sound.current.play();
        timer.current.setCallback(() => {
            props.onHold();
            sound.current.play();
        });
        timer.current.start();
    };

    const release = (e: any) => {
        e.preventDefault();

        if (!isPressed.current) {
            return;
        }

        isPressed.current = false;
        setClasses("HoldableButton__released");

        props.onRelease();
        timer.current.stop();
    };

    useEffect(() => {
        spanRef.current.addEventListener("touchstart", press, {passive: false});
        spanRef.current.addEventListener("touchend", release, {passive: false});

        return () => {
            spanRef.current.removeEventListener("touchstart", press);
            spanRef.current.removeEventListener("touchend", release);
        }
    }, [props.off, props.disabled]);

    const className = `${serializeClasses(classes)} ${props.className}`;

    return (
        <span
            ref={spanRef}
            onMouseDown={press}
            onMouseUp={release}
            onMouseLeave={release}
            className={className}
        >
            {props.children}
        </span>
    );
});

export default HoldableButton;

↓ ArmButton.tsx

import React, { useEffect, useMemo } from "react";
import BlinkingButton from "@components/BlinkingButton";
import { PlayIcon, PauseIcon } from "./icons";
import { useClasses, serializeClasses } from "./useClasses";
import ArmButtonPressSoundPath from "./ArmButtonPress.mp3";
import ArmButtonBlinkSoundPath from "./ArmButtonBlink.mp3";
import "./ArmButton.scss";

type PropsType = {
    callback: () => void;
    mode: types.AlarmClockMode;
};

export default function ArmButton(props: PropsType) {
    const [classes, setClasses] = useClasses();

    useEffect(() => setClasses({
        ArmButton__isArmed: props.mode !== "idle"
    }), [props.mode]);

    const icon = useMemo(() => {
        return (props.mode === "idle") ? <PlayIcon/> : <PauseIcon/>;
    }, [props.mode]);

    return (
        <BlinkingButton
            onPress={props.callback}
            blinking={props.mode === "fired"}
            pressSound={ArmButtonPressSoundPath}
            blinkSound={ArmButtonBlinkSoundPath}
            className={serializeClasses(classes)}
        >
            {icon}
        </BlinkingButton>
    );
}

↓ BlinkingButton.tsx

import React, { useRef, useEffect } from "react";
import HighResolutionTimer from "@src/HighResolutionTimer";
import AudioManager, { Sound } from "@src/AudioManager";
import useConstructor from "@hooks/useConstructor";
import { useClasses, serializeClasses } from "./useClasses";

type PropsType = React.PropsWithChildren<{
    onPress:    Function;
    blinking:   boolean;
    pressSound: string;
    blinkSound: string;
    className:  string;
}>;

export default function BlinkingButton(props: PropsType) {
    const [classes, setClasses] = useClasses();

    const timer      = useRef<HighResolutionTimer>();
    const pressSound = useRef<Sound>();
    const blinkSound = useRef<Sound>();
    const isLit      = useRef(false);

    const blink = (lit: boolean) => {
        isLit.current = lit;
        setClasses({BlinkingButton__isLit: lit});
    }

    useConstructor(() => {
        timer.current = new HighResolutionTimer(500, 500, () => {
            blink(!isLit.current);
            blinkSound.current.playIf(isLit.current);
        });

        const audioManager = AudioManager.getInstance();
        pressSound.current = audioManager.createSound(props.pressSound);
        blinkSound.current = audioManager.createSound(props.blinkSound);
    });

    useEffect(() => {
        if (props.blinking) {
            timer.current.start();
        } else {
            timer.current.stop();
            blink(false);
        }
    }, [props.blinking]);

    const callback = (e: React.MouseEvent) => {
        props.onPress();
        pressSound.current.play();
    }

    const className = `${serializeClasses(classes)} ${props.className}`;

    return (
        <span
            onClick={callback}
            className={className}
        >
            {props.children}
        </span>
    );
}

Hooks

↓ makeUseClasses.ts

import { useState } from "react";
import { isString, isFunction } from "@src/utils";

type StringSet = Set<string>;
type GroupDictionary = {[key: number]: Set<string>};
type Spec = {[key: string]: {init: boolean; group: number; precedence: number;}};
type SetClassesArgument = string|types.BoolDictionary|UpdateFunction;
type SetClassesFunction = (...args: SetClassesArgument[]) => void;
type UseClassesFunction = (...initialState: string[]) => [StringSet, SetClassesFunction];
type UpdateFunction = (oldState: StringSet) => string|string[]|types.BoolDictionary;
type SerializeClassesFunction = (state: StringSet) => string;

export default function makeUseClasses(spec: Spec): [UseClassesFunction, SerializeClassesFunction] {
    const groups = getGroups(spec, Object.keys(spec));

    const useClasses = (...initialState: string[]): [StringSet, SetClassesFunction] => {
        checkNames(spec, ...initialState);

        const [classes, _setClasses] = useState<StringSet>(() => {
            const defaultState = Object.keys(spec).filter((key: string) => spec[key].init);
            return new Set([...defaultState, ...initialState]);
        });

        const setClasses = (...args: SetClassesArgument[]) => {
            _setClasses((oldState: StringSet) => {
                let [insert, remove] = parseArguments(oldState, ...args);

                checkNames(spec, ...insert);
                checkNames(spec, ...remove);

                const diff = getDiff(groups, getGroups(spec, [...insert]));
                remove = new Set([...remove, ...diff]);

                const newState = new Set([...oldState, ...insert].filter((key: string) => !remove.has(key)));
                return newState;
            });
        };

        return [classes, setClasses];
    }

    const serializeClasses = (state: StringSet) => {
        const sorted = [...state].sort((a: string, b: string) => {
            return spec[a].precedence - spec[b].precedence
        });
        const result = sorted.join(" ");
        return result;
    }

    return [useClasses, serializeClasses];
}

const parseArguments = (oldState: StringSet, ...args: SetClassesArgument[]) => {
    let insert = new Set<string>();
    let remove = new Set<string>();

    for (let x of args) {
        const value = isFunction(x) ? (x as UpdateFunction)(oldState) : x;

        if (isString(value)) {
            const name = value as string;
            insert.add(name);
        } else if (Array.isArray(value)) {
            const names = value as string[];
            insert = new Set([...insert, ...names]);
        } else {
            const dictionary = value as types.BoolDictionary;
            for (let key of Object.keys(value)) {
                (dictionary[key] ? insert : remove).add(key);
            }
        }
    }

    return [insert, remove];
}

const getDiff = (x: GroupDictionary, y: GroupDictionary): StringSet => {
    let result = new Set<string>();

    for (let key of Object.keys(y)) {
        const group = parseInt(key);
        if (group === 0) return;

        const a = x[group];
        const b = y[group];
        const c = [...a].filter((key: string) => !b.has(key));
        result = new Set([...result, ...c]);
    }

    return result;
}

const getGroups = (spec: Spec, keys: string[]) => {
    let result: {[key: number]: Set<string>} = {};

    for (let key of keys) {
        const group = spec[key].group;
        if (group in result) {
            result[group].add(key);
        } else {
            result[group] = new Set([key]);
        }
    }

    return result;
}

const checkNames = (spec: Spec,...names: string[]) => {
    for (let name of names) {
        console.assert(name in spec, `${name} not in spec.`);
    }
}

↓ makeUseGlobal.ts

import { useState, useEffect } from "react";

export default function makeUseGlobal<T>(initialState: T) {
    let globalState = initialState;
    const listeners = new Set();

    const setState = (value: T) => {
        globalState = value;
        listeners.forEach((listener: Function) => {
            listener();
        });
    }

    return (): [T, Function] => {
        const [state, _setState] = useState<T>(globalState);

        useEffect(() => {
            const listener = () => {
                _setState(globalState);
            }
            listeners.add(listener);
            listener();

            return () => {
                return () => listeners.delete(listener);
            }
        }, []);

        return [state, setState];
    }
}

↓ useConstructor.ts

import { useRef } from "react";

export default function useConstructor(callback: Function, args: any[] = []) {
    const hasBeenCalled = useRef(false);
    if (hasBeenCalled.current) {
        return;
    } else {
        callback(...args);
        hasBeenCalled.current = true;
    }
}

Misc

↓ HighResolutionTimer.ts

type Callback = (timer: HighResolutionTimer) => void;

export default class HighResolutionTimer {
    callback:    Callback;
    duration:    number;
    delay:       number;
    startTime:   number|undefined;
    currentTime: number|undefined;
    timeoutId:   number|undefined;
    totalTicks:  number = 0;
    deltaTime:   number = 0;

    constructor(duration: number, delay: number = 0, callback: Callback = null) {
        this.duration = duration;
        this.delay = delay;
        this.callback = callback;
    }

    reset() {
        this.startTime = this.currentTime = this.timeoutId = undefined;
        this.totalTicks = this.deltaTime = 0;
    }

    tick() {
        const lastTime = this.currentTime;
        this.currentTime = Date.now();

        if (this.startTime === undefined) {
            this.startTime = this.currentTime;
        }

        if (lastTime !== undefined) {
            this.deltaTime = this.currentTime - lastTime;
        }

        this.callback(this);

        const nextTick = this.duration - (this.currentTime - (this.startTime + (this.totalTicks * this.duration)));
        this.totalTicks++;

        this.timeoutId = window.setTimeout(() => this.tick(), nextTick);
    }

    start() {
        console.assert(this.callback !== null, "Timer callback was not set.");

        this.reset();

        this.timeoutId = window.setTimeout(() => this.tick(), this.delay);
    }

    stop() {
        if (this.timeoutId !== undefined) {
            clearTimeout(this.timeoutId);
            this.timeoutId = undefined;
        }
    }

    setCallback(callback: Callback) {
        this.callback = callback;
    }
}

↓ AudioManager.ts

type BufferInfo = {
    buffer: AudioBuffer|null,
    path: string;
    ready: boolean;
};

export default class AudioManager {
    static instance: AudioManager = null;
    context: AudioContext;
    buffers: {[path: string]: BufferInfo} = {};

    constructor() {
        this.context = new (window.AudioContext || window.webkitAudioContext)();
        this.buffers = {};
    }

    static getInstance() {
        if (AudioManager.instance === null) {
            AudioManager.instance = new AudioManager();
        }

        return AudioManager.instance;
    }

    load(bufferInfo: BufferInfo) {
        const request = new XMLHttpRequest();
        request.open("GET", bufferInfo.path);
        request.responseType = "arraybuffer";
        request.onload = () => {
            this.context.decodeAudioData(request.response, (buffer: AudioBuffer) => {
                bufferInfo.buffer = buffer;
                bufferInfo.ready = true;
            });
        }
        request.send();
    }

    createSound(url: string) {
        if (url in this.buffers) {
            return new Sound(this, this.buffers[url]);
        }

        const bufferInfo: BufferInfo = {buffer: null, path: url, ready: false};
        this.buffers[url] = bufferInfo;

        this.load(bufferInfo);

        return new Sound(this, bufferInfo);
    }
}

export class Sound {
    manager: AudioManager;
    bufferInfo: BufferInfo;
    currentSource: AudioBufferSourceNode;

    constructor(manager: AudioManager, bufferInfo: BufferInfo) {
        this.manager = manager;
        this.bufferInfo = bufferInfo;
        this.currentSource = null;
    }

    play() {
        if (!this.bufferInfo.ready) {
            return
        }

        if (this.currentSource !== null) {
            this.currentSource.stop();
        }

        const context = this.manager.context;
        this.currentSource = context.createBufferSource();
        this.currentSource.buffer = this.bufferInfo.buffer;
        this.currentSource.connect(context.destination);
        this.currentSource.start();
    }

    playIf(condition: boolean) {
        if (condition) {
            this.play()
        }
    }
}


Get this bounty!!!

#StackBounty: #react.js #typescript Alarm Clock with React.js

Bounty: 100

Preface

This is an Alarm Clock app. The way it works is pretty simple: you change the time by pressing the +/- buttons and set the time by pressing the play button. When the time is up, the alarm will ring at which point you can stop it by pressing the pause button. This question is already long as it is so I won’t be including everything required to run the application yourself. But if you’d like to do that, the project is hosted on Github and can be found here. A live version of the app is also available and can be found here.

Comments

In the the beginning, from thinking about how the application would look like, I identified five components:

  • a ChangeTimeButton component which would be the +/- buttons for changing the hour and minute;
  • an ArmButton component to set the alarm to ring at the specified time;
  • a Controls component that would contain the ArmButton and ChangeTimeButtons;
  • a Clock component which would simply display the time;
  • an App component that would contain the Clock and Controls components.

Components

After working with this structure for some time, I felt that ChangeTimeButton had become too complicated. It needed to be a button, which means it had to trigger some action after it was pressed; furthermore, it should be possible for the user to hold it in order to change the hour or minute continuously, and it’s appearance while being pressed should be different; it would need to handle both mouse and touch events; a sound should play when it was pressed, and so on. All of this, I thought, had more to do with the fact that this was a button (even if it was a specific type of button) than the fact that it would be used to change the time. So I refactored ChangeTimeButton into two components: one concerned with changing the time and another concerned with being a button. I also broke up ArmButton in a similar way.

Something else that made ChangeTimeButton complicated was that, after the alarm was armed, I did not want the user to still be able to change the time, so there would have to be some notion of ChangeTimeButton being off, which would mean that the user would not be able to interact with it, and that it’s appearance would need to change in order to convey that idea. Later, I also found out that I needed to prevent more than one ChangeTimeButton from being pressed at the same time, so it was necessary to maintain state about which one was currently being pressed and disallow all others from modifying the time. But this did not require a change in appearance, so I decided I needed to separate the notion of ChangeTimeButton being off from the notion of it being simply disabled.

Another problem I had was that, because the ChangeTimeButtons would be positioned on each side of the ArmButton, I couldn’t think of a good way to make a component that could contain all of them while maintaining state about which one was being pressed. The solution I came up with was to use a custom hook that internally calls setState on all ChangeTimeButtons with some global data. This allowed me to access and modify state shared by all instances of ChangeTimeButton without the need for a container component or a context.


Components

↓ App.tsx

import React, { useState, useRef, useCallback } from "react";
import Clock from "@components/Clock";
import Controls from "@components/Controls";
import useConstructor from "@hooks/useConstructor";
import HighResolutionTimer from "@src/HighResolutionTimer";
import { calcTimeUntilAlert, changeTime, getCurrentTime } from "@src/time";
import "./App.scss";

export default function App() {
    const [mode, setMode] = useState<types.AlarmClockMode>("idle");
    const [time, setTime] = useState<types.Time>();
    const timeoutId = useRef<number>();

    useConstructor(() => {
        const json = localStorage.getItem("time");

        let time;
        if (json === undefined) {
            time = getCurrentTime();
        } else {
            time = JSON.parse(json);
        }

        setTime(time);
    });

    const armButtonCallback = useCallback(() => {
        if (mode === "idle") {
            setMode("armed");

            let delta = calcTimeUntilAlert(time);
            timeoutId.current = window.setTimeout(() => {
                setMode("fired");
            }, delta);

            localStorage.setItem("time", JSON.stringify(time));
        } else {
            setMode("idle");
            clearTimeout(timeoutId.current);
        }
    }, [mode, time])

    const changeTimeButtonCallback = useCallback((type: types.ChangeTimeButtonType) => {
        const f = {
            "h+": (time: types.Time) => changeTime(time,  1,  0),
            "h-": (time: types.Time) => changeTime(time, -1,  0),
            "m+": (time: types.Time) => changeTime(time,  0,  1),
            "m-": (time: types.Time) => changeTime(time,  0, -1)
        }[type];
        setTime(time => f(time));
    }, []);

    return (
        <div className="outerContainer">
            <div className="innerContainer">
                <Clock time={time} />
                <Controls
                    mode={mode}
                    armButtonCallback={armButtonCallback}
                    changeTimeButtonCallback={changeTimeButtonCallback}
                />
            </div>
        </div>
    );
}

↓ Clock.tsx

import React from "react";
import { formatTime } from "@src/time";
import "./Clock.scss";

type PropsType = { time: types.Time };

export default function Clock(props: PropsType) {
    return (
        <div className="Clock">
            <span className="Clock_fg">{formatTime(props.time)}</span>
            <span className="Clock_bg">88:88</span>
        </div>
    );
};

↓ Controls.tsx

import React, { useEffect, useMemo } from "react";
import ArmButton from "@components/ArmButton";
import ChangeTimeButton from "@components/ChangeTimeButton";
import { useClasses, serializeClasses } from "./useClasses";
import "./Controls.scss";

type PropsType = {
    mode: types.AlarmClockMode;
    armButtonCallback: () => void;
    changeTimeButtonCallback: (type: types.ChangeTimeButtonType) => void;
};

export default function Controls(props: PropsType) {
    const {mode, armButtonCallback, changeTimeButtonCallback} = props;

    const [classes, setClasses] = useClasses();

    const isNotIdle = mode !== "idle";
    useEffect(() => setClasses({Controls__isNotIdle: isNotIdle}), [isNotIdle]);

    return (
        <div className={serializeClasses(classes)}>
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="h+"
                className="ChangeTimeButton__left"
            />
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="h-"
                className="ChangeTimeButton__left"
            />
            <ArmButton
                callback={armButtonCallback}
                mode={mode}
            />
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="m-"
                className="ChangeTimeButton__right"
            />
            <ChangeTimeButton
                callback={changeTimeButtonCallback}
                off={isNotIdle}
                type="m+"
                className="ChangeTimeButton__right"
            />
        </div>
    );
}

↓ ChangeTimeButton.tsx

import React, { memo, useMemo, useCallback } from "react";
import HoldableButton from "@components/HoldableButton";
import { PlusIcon, MinusIcon } from "./icons";
import usePressed from "./usePressed";
import ChangeTimeButtonPressAndHoldSoundPath from "./ChangeTimeButtonPressAndHold.mp3";
import "./ChangeTimeButton.scss";

type PropsType = {
    callback: (type: types.ChangeTimeButtonType) => void;
    off: boolean;
    type: types.ChangeTimeButtonType;
    className: string;
};

const ChangeTimeButton = memo((props: PropsType) => {
    const { callback, type, off, className } = props;

    const [pressed, setPressed] = usePressed();
    const disabled = pressed !== null && pressed !== type;

    const onPress   = useCallback(() => { callback(type); setPressed(type) }, [type]);
    const onRelease = useCallback(() => setPressed(null), []);
    const onHold    = useCallback(() => callback(type), [type]);

    const icon = useMemo(() => {
        return (type === "h+" || type === "m+") ? <PlusIcon/> : <MinusIcon/>;
    }, []);

    return (
        <HoldableButton
            onPress={onPress}
            onRelease={onRelease}
            onHold={onHold}
            disabled={disabled}
            off={off}
            sound={ChangeTimeButtonPressAndHoldSoundPath}
            className={`ChangeTimeButton ${className}`}
        >
            {icon}
        </HoldableButton>
    );
});

export default ChangeTimeButton;

↓ HoldableButton.tsx

import React, { memo, useEffect, useRef } from "react";
import useConstructor from "@hooks/useConstructor";
import { useClasses, serializeClasses } from "./useClasses";
import HighResolutionTimer from "@src/HighResolutionTimer";
import AudioManager, { Sound } from "@src/AudioManager";

type PropsType = React.PropsWithChildren<{
    onPress:   Function;
    onRelease: Function;
    onHold:    Function;
    disabled:  boolean;
    off:       boolean;
    sound:     string;
    className: string;
}>;

const HoldableButton = memo((props: PropsType) => {
    const [classes, setClasses] = useClasses();
    useEffect(() => setClasses({HoldableButton__off: props.off}), [props.off]);

    const spanRef   = useRef<HTMLAnchorElement>();
    const timer     = useRef<HighResolutionTimer>();
    const sound     = useRef<Sound>();
    const isPressed = useRef(false);

    useConstructor(() => {
        timer.current = new HighResolutionTimer(110, 400);

        const audioManager = AudioManager.getInstance();
        sound.current = audioManager.createSound(props.sound);
    });

    const press = (e: any) => {
        e.preventDefault();

        if (e.type === "mousedown" && (("buttons" in e && e.buttons !== 1) || ("which" in e && e.which !== 1))) {
            return;
        }

        if (props.off || props.disabled) {
            return;
        }

        isPressed.current = true;
        setClasses("HoldableButton__pressed");

        props.onPress();
        sound.current.play();
        timer.current.setCallback(() => {
            props.onHold();
            sound.current.play();
        });
        timer.current.start();
    };

    const release = (e: any) => {
        e.preventDefault();

        if (!isPressed.current) {
            return;
        }

        isPressed.current = false;
        setClasses("HoldableButton__released");

        props.onRelease();
        timer.current.stop();
    };

    useEffect(() => {
        spanRef.current.addEventListener("touchstart", press, {passive: false});
        spanRef.current.addEventListener("touchend", release, {passive: false});

        return () => {
            spanRef.current.removeEventListener("touchstart", press);
            spanRef.current.removeEventListener("touchend", release);
        }
    }, [props.off, props.disabled]);

    const className = `${serializeClasses(classes)} ${props.className}`;

    return (
        <span
            ref={spanRef}
            onMouseDown={press}
            onMouseUp={release}
            onMouseLeave={release}
            className={className}
        >
            {props.children}
        </span>
    );
});

export default HoldableButton;

↓ ArmButton.tsx

import React, { useEffect, useMemo } from "react";
import BlinkingButton from "@components/BlinkingButton";
import { PlayIcon, PauseIcon } from "./icons";
import { useClasses, serializeClasses } from "./useClasses";
import ArmButtonPressSoundPath from "./ArmButtonPress.mp3";
import ArmButtonBlinkSoundPath from "./ArmButtonBlink.mp3";
import "./ArmButton.scss";

type PropsType = {
    callback: () => void;
    mode: types.AlarmClockMode;
};

export default function ArmButton(props: PropsType) {
    const [classes, setClasses] = useClasses();

    useEffect(() => setClasses({
        ArmButton__isArmed: props.mode !== "idle"
    }), [props.mode]);

    const icon = useMemo(() => {
        return (props.mode === "idle") ? <PlayIcon/> : <PauseIcon/>;
    }, [props.mode]);

    return (
        <BlinkingButton
            onPress={props.callback}
            blinking={props.mode === "fired"}
            pressSound={ArmButtonPressSoundPath}
            blinkSound={ArmButtonBlinkSoundPath}
            className={serializeClasses(classes)}
        >
            {icon}
        </BlinkingButton>
    );
}

↓ BlinkingButton.tsx

import React, { useRef, useEffect } from "react";
import HighResolutionTimer from "@src/HighResolutionTimer";
import AudioManager, { Sound } from "@src/AudioManager";
import useConstructor from "@hooks/useConstructor";
import { useClasses, serializeClasses } from "./useClasses";

type PropsType = React.PropsWithChildren<{
    onPress:    Function;
    blinking:   boolean;
    pressSound: string;
    blinkSound: string;
    className:  string;
}>;

export default function BlinkingButton(props: PropsType) {
    const [classes, setClasses] = useClasses();

    const timer      = useRef<HighResolutionTimer>();
    const pressSound = useRef<Sound>();
    const blinkSound = useRef<Sound>();
    const isLit      = useRef(false);

    const blink = (lit: boolean) => {
        isLit.current = lit;
        setClasses({BlinkingButton__isLit: lit});
    }

    useConstructor(() => {
        timer.current = new HighResolutionTimer(500, 500, () => {
            blink(!isLit.current);
            blinkSound.current.playIf(isLit.current);
        });

        const audioManager = AudioManager.getInstance();
        pressSound.current = audioManager.createSound(props.pressSound);
        blinkSound.current = audioManager.createSound(props.blinkSound);
    });

    useEffect(() => {
        if (props.blinking) {
            timer.current.start();
        } else {
            timer.current.stop();
            blink(false);
        }
    }, [props.blinking]);

    const callback = (e: React.MouseEvent) => {
        props.onPress();
        pressSound.current.play();
    }

    const className = `${serializeClasses(classes)} ${props.className}`;

    return (
        <span
            onClick={callback}
            className={className}
        >
            {props.children}
        </span>
    );
}

Hooks

↓ makeUseClasses.ts

import { useState } from "react";
import { isString, isFunction } from "@src/utils";

type StringSet = Set<string>;
type GroupDictionary = {[key: number]: Set<string>};
type Spec = {[key: string]: {init: boolean; group: number; precedence: number;}};
type SetClassesArgument = string|types.BoolDictionary|UpdateFunction;
type SetClassesFunction = (...args: SetClassesArgument[]) => void;
type UseClassesFunction = (...initialState: string[]) => [StringSet, SetClassesFunction];
type UpdateFunction = (oldState: StringSet) => string|string[]|types.BoolDictionary;
type SerializeClassesFunction = (state: StringSet) => string;

export default function makeUseClasses(spec: Spec): [UseClassesFunction, SerializeClassesFunction] {
    const groups = getGroups(spec, Object.keys(spec));

    const useClasses = (...initialState: string[]): [StringSet, SetClassesFunction] => {
        checkNames(spec, ...initialState);

        const [classes, _setClasses] = useState<StringSet>(() => {
            const defaultState = Object.keys(spec).filter((key: string) => spec[key].init);
            return new Set([...defaultState, ...initialState]);
        });

        const setClasses = (...args: SetClassesArgument[]) => {
            _setClasses((oldState: StringSet) => {
                let [insert, remove] = parseArguments(oldState, ...args);

                checkNames(spec, ...insert);
                checkNames(spec, ...remove);

                const diff = getDiff(groups, getGroups(spec, [...insert]));
                remove = new Set([...remove, ...diff]);

                const newState = new Set([...oldState, ...insert].filter((key: string) => !remove.has(key)));
                return newState;
            });
        };

        return [classes, setClasses];
    }

    const serializeClasses = (state: StringSet) => {
        const sorted = [...state].sort((a: string, b: string) => {
            return spec[a].precedence - spec[b].precedence
        });
        const result = sorted.join(" ");
        return result;
    }

    return [useClasses, serializeClasses];
}

const parseArguments = (oldState: StringSet, ...args: SetClassesArgument[]) => {
    let insert = new Set<string>();
    let remove = new Set<string>();

    for (let x of args) {
        const value = isFunction(x) ? (x as UpdateFunction)(oldState) : x;

        if (isString(value)) {
            const name = value as string;
            insert.add(name);
        } else if (Array.isArray(value)) {
            const names = value as string[];
            insert = new Set([...insert, ...names]);
        } else {
            const dictionary = value as types.BoolDictionary;
            for (let key of Object.keys(value)) {
                (dictionary[key] ? insert : remove).add(key);
            }
        }
    }

    return [insert, remove];
}

const getDiff = (x: GroupDictionary, y: GroupDictionary): StringSet => {
    let result = new Set<string>();

    for (let key of Object.keys(y)) {
        const group = parseInt(key);
        if (group === 0) return;

        const a = x[group];
        const b = y[group];
        const c = [...a].filter((key: string) => !b.has(key));
        result = new Set([...result, ...c]);
    }

    return result;
}

const getGroups = (spec: Spec, keys: string[]) => {
    let result: {[key: number]: Set<string>} = {};

    for (let key of keys) {
        const group = spec[key].group;
        if (group in result) {
            result[group].add(key);
        } else {
            result[group] = new Set([key]);
        }
    }

    return result;
}

const checkNames = (spec: Spec,...names: string[]) => {
    for (let name of names) {
        console.assert(name in spec, `${name} not in spec.`);
    }
}

↓ makeUseGlobal.ts

import { useState, useEffect } from "react";

export default function makeUseGlobal<T>(initialState: T) {
    let globalState = initialState;
    const listeners = new Set();

    const setState = (value: T) => {
        globalState = value;
        listeners.forEach((listener: Function) => {
            listener();
        });
    }

    return (): [T, Function] => {
        const [state, _setState] = useState<T>(globalState);

        useEffect(() => {
            const listener = () => {
                _setState(globalState);
            }
            listeners.add(listener);
            listener();

            return () => {
                return () => listeners.delete(listener);
            }
        }, []);

        return [state, setState];
    }
}

↓ useConstructor.ts

import { useRef } from "react";

export default function useConstructor(callback: Function, args: any[] = []) {
    const hasBeenCalled = useRef(false);
    if (hasBeenCalled.current) {
        return;
    } else {
        callback(...args);
        hasBeenCalled.current = true;
    }
}

Misc

↓ HighResolutionTimer.ts

type Callback = (timer: HighResolutionTimer) => void;

export default class HighResolutionTimer {
    callback:    Callback;
    duration:    number;
    delay:       number;
    startTime:   number|undefined;
    currentTime: number|undefined;
    timeoutId:   number|undefined;
    totalTicks:  number = 0;
    deltaTime:   number = 0;

    constructor(duration: number, delay: number = 0, callback: Callback = null) {
        this.duration = duration;
        this.delay = delay;
        this.callback = callback;
    }

    reset() {
        this.startTime = this.currentTime = this.timeoutId = undefined;
        this.totalTicks = this.deltaTime = 0;
    }

    tick() {
        const lastTime = this.currentTime;
        this.currentTime = Date.now();

        if (this.startTime === undefined) {
            this.startTime = this.currentTime;
        }

        if (lastTime !== undefined) {
            this.deltaTime = this.currentTime - lastTime;
        }

        this.callback(this);

        const nextTick = this.duration - (this.currentTime - (this.startTime + (this.totalTicks * this.duration)));
        this.totalTicks++;

        this.timeoutId = window.setTimeout(() => this.tick(), nextTick);
    }

    start() {
        console.assert(this.callback !== null, "Timer callback was not set.");

        this.reset();

        this.timeoutId = window.setTimeout(() => this.tick(), this.delay);
    }

    stop() {
        if (this.timeoutId !== undefined) {
            clearTimeout(this.timeoutId);
            this.timeoutId = undefined;
        }
    }

    setCallback(callback: Callback) {
        this.callback = callback;
    }
}

↓ AudioManager.ts

type BufferInfo = {
    buffer: AudioBuffer|null,
    path: string;
    ready: boolean;
};

export default class AudioManager {
    static instance: AudioManager = null;
    context: AudioContext;
    buffers: {[path: string]: BufferInfo} = {};

    constructor() {
        this.context = new (window.AudioContext || window.webkitAudioContext)();
        this.buffers = {};
    }

    static getInstance() {
        if (AudioManager.instance === null) {
            AudioManager.instance = new AudioManager();
        }

        return AudioManager.instance;
    }

    load(bufferInfo: BufferInfo) {
        const request = new XMLHttpRequest();
        request.open("GET", bufferInfo.path);
        request.responseType = "arraybuffer";
        request.onload = () => {
            this.context.decodeAudioData(request.response, (buffer: AudioBuffer) => {
                bufferInfo.buffer = buffer;
                bufferInfo.ready = true;
            });
        }
        request.send();
    }

    createSound(url: string) {
        if (url in this.buffers) {
            return new Sound(this, this.buffers[url]);
        }

        const bufferInfo: BufferInfo = {buffer: null, path: url, ready: false};
        this.buffers[url] = bufferInfo;

        this.load(bufferInfo);

        return new Sound(this, bufferInfo);
    }
}

export class Sound {
    manager: AudioManager;
    bufferInfo: BufferInfo;
    currentSource: AudioBufferSourceNode;

    constructor(manager: AudioManager, bufferInfo: BufferInfo) {
        this.manager = manager;
        this.bufferInfo = bufferInfo;
        this.currentSource = null;
    }

    play() {
        if (!this.bufferInfo.ready) {
            return
        }

        if (this.currentSource !== null) {
            this.currentSource.stop();
        }

        const context = this.manager.context;
        this.currentSource = context.createBufferSource();
        this.currentSource.buffer = this.bufferInfo.buffer;
        this.currentSource.connect(context.destination);
        this.currentSource.start();
    }

    playIf(condition: boolean) {
        if (condition) {
            this.play()
        }
    }
}


Get this bounty!!!

#StackBounty: #seo #web-crawlers #javascript #react-js #dom Using display: none VS a SPA approach, when it comes to SEO and performance

Bounty: 100

Say I have a component like,

// first approach (SPA)
const Component = () => {
  const [show, toggle] = React.useReducer(show => !show, false)

  return (
    <div>
      {show && <div>content1</div>}
      {!show && <div>content2</div>}
    </div>
  )
}

And say I decide to rewrite it as,

// second approach (toggling 'display' attribute)
const Component = () => {
  const [show, toggle] = React.useReducer(show => !show, false)

  return (
    <div>
      <div style={!show ? { display: 'none' } : {}}>content1</div>
      <div style={show ? { display: 'none' } : {}}>content2</div>
    </div>
  )
}

I’m not 100% sure if the first approach would be considered ‘SPA,’ but that’s usually how I write my SPAs, so I’ll just refer to it as such; but I digress.

My question is, as far as SEO and performance are concerned, which would be a better approach in each case?

I know web crawlers love content. If I’m not mistaken, web crawlers wouldn’t be able to see the hidden div in the SPA approach, so they’d miss/not see this content. But in the second approach, the DOM nodes are there — just the currently ‘hidden’ div is not visible to the user. And it’s not like I’m setting visibility to hidden, so that’s why I’m assuming web crawlers can see the content, which makes it good for SEO.

As far as performance is concerned, I would imagine the first SPA approach would be much better suited in that respect, because the DOM would be less populated.

Is my thinking correct? Would taking the second approach be better for SEO and, if so, would it be at the expense of performance?


Get this bounty!!!

#StackBounty: #javascript #ecmascript-6 #react.js Working with images in multiple formats

Bounty: 50

I’ve been doing some research around how to best make use of the WebP images (with fallback) in React. Most solutions point to something similar to what I’ve written below. That means if I have a lot of images I have to export the same image twice in assets.js (both PNG and WebP), import twice and provide two Image props for the same image.

This feels very inefficient. Is this an uncommon way of working with images? Is there a better way for working with images that live locally?

assets.js – where I collect all my assets and export them as strings. Imagine this list being really lengthy.

export { default as image1 } from "./assets/image-1.png";
export { default as image1Webp } from "./assets/image-1.webp";

In the file I import the assets I need for that page

import {
    image1,
    image1Webp
} from "./assets";

The component + props

<Image
  src={image1Webp}
  fallback={image1}
  alt="Lorem"
/>

Image.js component

const Image = ({
  src,
  fallback,
  type = 'image/webp',
  ...delegated
}) => {
  return (
    <picture>
      <source srcSet={src} type={type} />
      <img src={fallback} {...delegated} />
    </picture>
  );
};

Some things I’ve tried:

Generally the recommended way is to add images to a React project is to use require(img/path). So I tried things like appending .png or .webp to the base URL, inside the Image component. so I only need to define one string and then the component handles the splitting.

e.g. pseudocode:

export const toWEBP = (src) => {
  const location = src.split(".")
  return `${location}.webp`
}


Get this bounty!!!

#StackBounty: #javascript #react.js Working with images in multiple formats

Bounty: 50

I’ve been doing some research around how to best make use of the WebP images (with fallback) in React. Most solutions point to something similar to what I’ve written below. That means if I have a lot of images I have to export the same image twice in assets.js (both PNG and WebP), import twice and provide two Image props for the same image.

This feels very inefficient. Is this an uncommon way of working with images? Is there a better way for working with images that live locally?

assets.js – where I collect all my assets and export them as strings. Imagine this list being really lengthy.

export { default as image1 } from "./assets/image-1.png";
export { default as image1Webp } from "./assets/image-1.webp";

In the file I import the assets I need for that page

import {
    image1,
    image1Webp
} from "./assets";

The component + props

<Image
  src={image1Webp}
  fallback={image1}
  alt="Lorem"
/>

Image.js component

const Image = ({
  src,
  fallback,
  type = 'image/webp',
  ...delegated
}) => {
  return (
    <picture>
      <source srcSet={src} type={type} />
      <img src={fallback} {...delegated} />
    </picture>
  );
};

Some things I’ve tried:

Generally the recommended way is to add images to a React project is to use require(img/path). So I tried things like appending .png or .webp to the base URL, inside the Image component. so I only need to define one string and then the component handles the splitting.

e.g. pseudocode:

export const toWEBP = (src) => {
  const location = src.split(".")
  return `${location}.webp`
}


Get this bounty!!!

#StackBounty: #react.js #typescript Extending react hook useState for use in event handlers

Bounty: 50

I use React with material-ui.com and I love them both, but I’m tired with writing the boilerplate handlers like

onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.currentTarget.value)}

again and again. Especially when stopPropagation si needed (AFAICT it’s always either needed or harmless), it gets pretty verbose.

So I tried to extend useState so that it provides everything I need, so I can write things like

const [email, setEmail, emailAgg] = useStateEx('');
...
onChange={emailAgg.changeHandler}

or

const [showPassword, , showPasswordAgg] = useStateEx(false);
...
onClick={showPasswordAgg.toggleHandler}

and similar.


My approach follows. I’d like it to be reviewed in general, especially for better typing (I had to use @ts-ignore) and improvements.

I case you hate semicolons, please ignore them as my eslint is set up to require them. 😉

import { useState, Dispatch, SetStateAction } from 'react';

function booleanAgg(value: boolean, setter: (value: boolean) => void, makeSetter: (value: boolean) => ((event?: any) => void)) {
    return {
        value,
        setter,
        falseHandler: makeSetter(false),
        trueHandler: makeSetter(true),
        toggleHandler: makeSetter(!value),
    };
}

function stringAgg(value: string, setter: (value: string) => void) {
    return {
        value,
        setter,
        changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
            e.stopPropagation();
            setter(e.currentTarget.value);
        }
    };
}

function numberAgg(value: number, setter: (value: number) => void) {
    return {
        value,
        setter,
        changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
            e.stopPropagation();
            setter(+e.currentTarget.value);
        }
    };
}

/**
 * Works like useState and adds a third element containing boolean-specific handlers directly useable in buttons and checkboxes.
 */
export function useStateEx(initialState: boolean) : [boolean, Dispatch<SetStateAction<boolean>>, ReturnType<typeof booleanAgg>];

/**
 * Works like useState and adds a third element containing string-specific handlers directly useable in text fields.
 */
export function useStateEx(initialState: string) : [string, Dispatch<SetStateAction<string>>, ReturnType<typeof stringAgg>];

/**
 * Works like useState and adds a third element containing number-specific handlers directly useable in text fields.
 */
export function useStateEx(initialState: number) : [number, Dispatch<SetStateAction<number>>, ReturnType<typeof numberAgg>];

/**
 * Prohibits use with any type not handled in the above overloads.
 */
export function useStateEx(initialState: any) : never;

export function useStateEx<S extends boolean|string|number>(initialState: S) : unknown {
    const [value, setter] = useState(initialState);
    function makeSetter(value: S) {
        return function(e: any) {
            if (typeof e?.stopPropagation === 'function') e.stopPropagation();
            setter(value);
        };
    }
    if (typeof value === 'boolean') {
        // @ts-ignore
        return [value, setter, booleanAgg(value, setter, makeSetter)]
    } else if (typeof value === 'string') {
        // @ts-ignore
        return [value, setter, stringAgg(value, setter)];
    } else if (typeof value === 'number') {
        // @ts-ignore
        return [value, setter, numberAgg(value, setter)];
    } else {
        throw new Error('Only boolean, string and number is supported');
    }
}


Get this bounty!!!

#StackBounty: #javascript #react.js #typescript #redux React Context & Hooks custom Vuex like store

Bounty: 50

I’ve been experimenting with Hooks lately and looking more into how can I replace Redux with useContext and useReduer. For me, Vuex was more intuitive when I first got into stores and I prefer their state management pattern, so I tried to build something close to that using the React Context.

I aim to have one store/context for each page in my app or to have the ability to pull the store/context up, so it’s available globally if needed. The final custom useStore() hook should return a store with the following parts:

{ state, mutations, actions, getters }

enter image description here

Components can then dispatch actions with actions.dispatch({type: 'my-action', payload}) (actions commit mutations) or directly commit mutations with mutations.commit({ type: 'my-mutation', payload}). Mutations then mutate the state (using useReducer), which finally causes a rerender.

For my example, I have two entities inside ./models. User (context/store provided globally) and Post(context/store provided on it’s page):

// User.ts
export interface User {
  id: number
  username: string
  website: string
}

// Post.ts
export interface Post {
  id: number
  userId: number
  title: string
}

I then create the reducers ./store/{entity}/structure/reducer.ts:

import { UserState } from './types'
import { UserMutations } from './types';

export function userReducer(state: UserState, mutation: UserMutations): UserState {
  switch (mutation.type) {
    // ...
    case 'set-users':
      return { ...state, users: [...state.users, ...mutation.users] }
    // ...
  }
}

Switch through mutations from ./store/{entity}/structure/mutations.ts

import { User } from '../../../models/User';
import { AxiosError } from 'axios';

export const setUsers = (users: User[]) => ({
  type: 'set-users',
  users
} as const);

To get the state ./store/{entity}/structure/types/index.ts:

export interface UserState {
  isLoading: boolean
  error: AxiosError
  users: User[]
}

Any heavier work (fetching data, etc.) before committing a mutation is located inside actions ./store/{entity}/structure/actions.ts:

import { UserMutations, UserActions } from "./types";
import axios, { AxiosResponse } from 'axios';
import { GET_USERS_URL, User } from "../../../models/User";
import { API_BASE_URL } from "../../../util/utils";

export const loadUsers = () => ({
  type: 'load-users'
} as const);

export const initActions = (commit: React.Dispatch<UserMutations>) => {
  const dispatch: React.Dispatch<UserActions> = async (action) => {
    switch (action.type) {
      case 'load-users':
        try {
          commit({ type: 'set-loading', isLoading: true })
          const res: AxiosResponse<User[]> = await axios.get(`${API_BASE_URL}${GET_USERS_URL}`)

          if (res.status === 200) {
            const users: User[] = res.data.map((apiUser) => ({
              id: apiUser.id,
              username: apiUser.username,
              website: apiUser.website
            }))

            commit({ type: 'set-users', users })
          }
        } catch (error) {
          commit({ type: 'set-error', error })
        } finally {
          commit({ type: 'set-loading', isLoading: false })
        }
        break;

      default:
        break;
    }
  }

  return dispatch
}

Additionally, a new derived state can be computed based on store state using getters ./store/{entity}/structure/getters.ts:

import { UserState, UserGetters } from "./types"

export const getters = (state: Readonly<UserState>): UserGetters => {
  return {
    usersReversed: [...state.users].reverse()
  }
}

Finally, everything is initialized and glued together inside ./store/{entity}/Context.tsx:

import React, { createContext, useReducer } from 'react'
import { UserStore, UserState } from './structure/types'
import { userReducer } from './structure/reducer'
import { getters } from './structure/getters'
import { initActions } from './structure/actions'
import { AxiosError } from 'axios'

const initialStore: UserStore = {
  state: {
    isLoading: false,
    error: {} as AxiosError,
    users: []
  } as UserState,
  getters: {
    usersReversed: []
  },
  mutations: {
    commit: () => {}
  },
  actions: {
    dispatch: () => {}
  }
}

export const UserContext = createContext<UserStore>(initialStore)

export const UserContextProvider: React.FC = (props) => {
  const [state, commit] = useReducer(userReducer, initialStore.state)
  const store: UserStore = {
    state,
    getters: getters(state),
    actions: {
      dispatch: initActions(commit)
    },
    mutations: {
      commit
    }
  }

  return (
    <UserContext.Provider value={store}>
      {props.children}
    </UserContext.Provider>
  )
}

For a syntactic sugar, I wrap the useContext() hook with a custom one:

import { useContext } from 'react'
import { UserContext } from './UserContext'

const useUserStore = () => {
  return useContext(UserContext)
}

export default useUserStore

After providing the context, the store can be used as such:

const { actions, getters, mutations, state } = useUserStore()

useEffect(() => {
  actions.dispatch({ type: 'load-users' })
}, [])

Are there any optimizations I can do? What are the biggest cons when comparing to redux? Here is the repo, any feedback is appreciated.

Edit new

Edit 1:

I’ve wrapped the useContext() with a custom useUserStore() hook, so it can be used as

const { actions, getters, mutations, state } = useUserStore()

and so the store/context terms are unified when using the store.


Get this bounty!!!

#StackBounty: #seo #single-page-application #react-js Impact of query parameters on SEO for a single page application

Bounty: 100

Might crawlers visit a page if there is no link referencing this page anywhere but a URL to this page is generated client side with JavaScript ?

Context:

Let’s say I have a SPA with Server Side Rendering. Some pages show a list of items and offer a filtering facility. When the user selects some options or fills in some input field to filter the list, I’d like to embed this information in the URL (eg. /items?sort=price&order=desc&q=something) via the history API (client side routing). Behind the scene, an API call is made to get the results.

Since I do SSR, the server will also be able to understand these URLs and render these pages (hence the user can bookmark the page or share it). But nowhere in the HTML pages these URLs will appear, there are only generated client side in response to user events.

In this context, I think crawlers won’t know these pages exist, and so, they should have no impact on SEO. Even if crawlers are now able to run JavaScript, they don’t use it to simulate user events.

Am I wrong ?

(I guess if someone shares publicly that kind of URL, it could suffice to make this page crawled ? In any case, what I’m worried about is the cost on the crawling budget if all these pages are visited, but I’m ok with a few pages being crawled, they could be marked as “noindex” for instance).


Get this bounty!!!