Skip to the code
cf.

Once

Write once(fn). It returns a function that calls fn the first time it's called, with that call's this and arguments, and returns the result. Every later call returns the same first result without calling fn again, whatever arguments it gets.

The first result is remembered even when it's undefined, 0, or another falsy value. If fn calls the wrapper again while it's running, that inner call doesn't run fn a second time. Each call to once makes an independent wrapper.

const init = once((config) => {
  console.log("connecting to", config.url);
  return { connected: true };
});
init({ url: "a" }); // logs "connecting to a", returns { connected: true }
init({ url: "b" }); // returns the same object, logs nothing

What the tests check

  1. calls fn on the first call and returns its result
    const fn = spy(() => 42);
    const init = once(fn);
    expect(init()).toBe(42);
    expect(fn).toHaveBeenCalledTimes(1);
    
  2. doesn't call fn again on later calls
    const fn = spy();
    const init = once(fn);
    init();
    init();
    init();
    expect(fn).toHaveBeenCalledTimes(1);
    
  3. later calls return the first result, whatever their arguments
    const double = once((x) => x * 2);
    expect(double(5)).toBe(10);
    expect(double(100)).toBe(10);
    const make = once(() => ({}));
    expect(make()).toBe(make());
    
  4. passes the first call's arguments
    const fn = spy();
    once(fn)("a", 1);
    expect(fn).toHaveBeenCalledWith("a", 1);
    
  5. keeps the caller's this
    const obj = {
      x: 7,
      getX: once(function () {
        return this.x;
      }),
    };
    expect(obj.getX()).toBe(7);
    
  6. remembers a falsy result without calling fn again
    const nothing = spy(() => undefined);
    const init = once(nothing);
    init();
    init();
    expect(nothing).toHaveBeenCalledTimes(1);
    const zero = spy(() => 0);
    const getZero = once(zero);
    getZero();
    expect(getZero()).toBe(0);
    expect(zero).toHaveBeenCalledTimes(1);
    
  7. a call from inside fn doesn't run fn again
    let init;
    const fn = spy(() => {
      init();
      return "done";
    });
    init = once(fn);
    expect(init()).toBe("done");
    expect(fn).toHaveBeenCalledTimes(1);
    
  8. each wrapper is independent
    const fn = spy((x) => x);
    const first = once(fn);
    const second = once(fn);
    expect(first(1)).toBe(1);
    expect(second(2)).toBe(2);
    expect(fn).toHaveBeenCalledTimes(2);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).