export function AsyncNode({ children }: { children: Promise<ReactNode> }) {
  return useLatestResolvedValue<ReactNode>(children, () => null) as any;
  // as any assertion needed for issue https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20356#issuecomment-336384210
}

export function useLatestResolvedValue<Value>(
  promise: Promise<Value>,
  initial: () => Value
) {
  const [[value], dispatch] = useReducer<
    Reducer<
      [Value, Array<Promise<Value>>],
      | { type: "enqueue"; promise: Promise<Value> }
      | { type: "resolved"; promise: Promise<Value>; value: Value }
    >
  >(latestResolvedPromiseReducer, [initial(), []]);
  useEffect(() => {
    dispatch({ type: "enqueue", promise });
    promise.then(node => dispatch({ type: "resolved", value: node, promise }));
  }, [promise]);
  return value;
}

function latestResolvedPromiseReducer<Value>(
  [display, queue]: [Value, Array<Promise<Value>>],
  action:
    | { type: "enqueue"; promise: Promise<Value> }
    | { type: "resolved"; promise: Promise<Value>; value: Value }
): [Value, Array<Promise<Value>>] {
  switch (action.type) {
    case "enqueue":
      return [display, queue.concat(action.promise)];
    case "resolved": {
      const index = queue.indexOf(action.promise);
      if (index === -1) return [display, queue];
      return [action.value, queue.slice(index)];
    }
    default:
      throw new Error();
  }
}

// examples

const example1 = <AsyncNode>{Promise.resolve(<h1>hi</h1>)}</AsyncNode>;
const example2 = <RandomDelay />;

async function callApi(text: string) {
  await new Promise(resolve => setTimeout(resolve, Math.random() * 3000));
  return `response for ${text}`;
}

function RandomDelay() {
  const [text, setText] = useState("");
  const response = useMemo(() => callApi(text), [text]);
  const content = useLatestResolvedValue(response, () => null);
  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} /><br/>
      <AsyncNode>{response}</AsyncNode><br/>
      {content}
    </div>
  );
}
