diff --git a/denops/ddu/app.ts b/denops/ddu/app.ts
index 5e348f4..dffbacc 100644
--- a/denops/ddu/app.ts
+++ b/denops/ddu/app.ts
@@ -63,8 +63,7 @@ export const main: Entrypoint = (denops: Denops) => {
     actions: [],
   };
   const lock = new Lock(0);
-  let queuedName: string | null = null;
-  let queuedRedrawOption: RedrawOption | null = null;
+  const uiRedrawLock = new Lock(0);
 
   const checkDdu = (name: string) => {
     if (!ddus[name]) {
@@ -82,7 +81,7 @@ export const main: Entrypoint = (denops: Denops) => {
   };
   const getDdu = (name: string) => {
     if (!checkDdu(name)) {
-      ddus[name].push(new Ddu(getLoader(name)));
+      ddus[name].push(new Ddu(getLoader(name), uiRedrawLock));
     }
 
     return ddus[name].slice(-1)[0];
@@ -90,7 +89,7 @@ export const main: Entrypoint = (denops: Denops) => {
   const pushDdu = (name: string) => {
     checkDdu(name);
 
-    ddus[name].push(new Ddu(getLoader(name)));
+    ddus[name].push(new Ddu(getLoader(name), uiRedrawLock));
 
     return ddus[name].slice(-1)[0];
   };
@@ -358,57 +357,49 @@ export const main: Entrypoint = (denops: Denops) => {
       return items;
     },
     async redraw(arg1: unknown, arg2: unknown): Promise<void> {
-      queuedName = ensure(arg1, is.String) as string;
-      queuedRedrawOption = ensure(arg2, is.Record) as RedrawOption;
+      const name = ensure(arg1, is.String) as string;
+      const opt = ensure(arg2, is.Record) as RedrawOption;
 
-      // NOTE: must be locked
-      await lock.lock(async () => {
-        while (queuedName !== null) {
-          const name = queuedName;
-          const opt = queuedRedrawOption;
-          queuedName = null;
-          queuedRedrawOption = null;
-
-          const ddu = getDdu(name);
-          const loader = getLoader(name);
-
-          if (opt?.check && !(await ddu.checkUpdated(denops))) {
-            // Mtime check failed
-            continue;
-          }
+      const ddu = getDdu(name);
+      const loader = getLoader(name);
+      let signal = ddu.cancelled;
 
-          if (opt?.input !== undefined) {
-            await ddu.setInput(denops, opt.input);
-          }
+      if (opt?.check && !(await ddu.checkUpdated(denops))) {
+        // Mtime check failed
+        return;
+      }
 
-          // Check volatile sources
-          const volatiles = ddu.getSourceArgs().map(
-            (sourceArgs, index) => sourceArgs[0].volatile ? index : -1,
-          ).filter((index) => index >= 0);
+      if (opt?.input !== undefined) {
+        await ddu.setInput(denops, opt.input);
+      }
 
-          if (volatiles.length > 0 || opt?.method === "refreshItems") {
-            await ddu.refresh(
-              denops,
-              opt?.method === "refreshItems" ? [] : volatiles,
-            );
-          } else if (opt?.method === "uiRedraw") {
-            await ddu.uiRedraw(denops);
-          } else {
-            await ddu.redraw(denops);
-          }
-          await ddu.restoreTree(denops);
-
-          if (opt?.searchItem) {
-            await uiSearchItem(
-              denops,
-              loader,
-              ddu.getContext(),
-              ddu.getOptions(),
-              opt.searchItem,
-            );
-          }
+      if (opt?.method === "refreshItems") {
+        signal = await ddu.refresh(denops, [], { restoreTree: true });
+      } else {
+        // Check volatile sources
+        const volatiles = ddu.getSourceArgs().map(
+          (sourceArgs, index) => sourceArgs[0].volatile ? index : -1,
+        ).filter((index) => index >= 0);
+
+        if (volatiles.length > 0) {
+          signal = await ddu.refresh(denops, volatiles, { restoreTree: true });
+        } else if (opt?.method === "uiRedraw") {
+          await ddu.restoreTree(denops, { preventRedraw: true, signal });
+          await ddu.uiRedraw(denops, { signal });
+        } else {
+          await ddu.redraw(denops, { restoreTree: true, signal });
         }
-      });
+      }
+
+      if (opt?.searchItem && !signal.aborted) {
+        await uiSearchItem(
+          denops,
+          loader,
+          ddu.getContext(),
+          ddu.getOptions(),
+          opt.searchItem,
+        );
+      }
     },
     async redrawTree(
       arg1: unknown,
diff --git a/denops/ddu/ddu.ts b/denops/ddu/ddu.ts
index ebc6819..cb74ade 100644
--- a/denops/ddu/ddu.ts
+++ b/denops/ddu/ddu.ts
@@ -25,9 +25,17 @@ import { defaultSourceOptions } from "./base/source.ts";
 import type { BaseSource } from "./base/source.ts";
 import type { Loader } from "./loader.ts";
 import { convertUserString, printError, treePath2Filename } from "./utils.ts";
-import type { AvailableSourceInfo, GatherStateAbortable } from "./state.ts";
-import { GatherState } from "./state.ts";
-import { isRefreshTarget } from "./state.ts";
+import type {
+  AvailableSourceInfo,
+  GatherStateAbortable,
+  GatherStateAbortReason,
+} from "./state.ts";
+import {
+  GatherState,
+  isRefreshTarget,
+  QuitAbortReason,
+  RefreshAbortReason,
+} from "./state.ts";
 import {
   callColumns,
   callFilters,
@@ -51,7 +59,7 @@ import * as fn from "jsr:@denops/std@~7.4.0/function";
 import { assertEquals } from "jsr:@std/assert@~1.0.2/equals";
 import { equal } from "jsr:@std/assert@~1.0.2/equal";
 import { basename } from "jsr:@std/path@~1.0.2/basename";
-import { Lock } from "jsr:@core/asyncutil@~1.2.0/lock";
+import type { Lock } from "jsr:@core/asyncutil@~1.2.0/lock";
 import { SEPARATOR as pathsep } from "jsr:@std/path@~1.0.2/constants";
 
 type RedrawOptions = {
@@ -60,9 +68,12 @@ type RedrawOptions = {
    * item's states reset to gathered.
    */
   restoreItemState?: boolean;
+  restoreTree?: boolean;
   signal?: AbortSignal;
 };
 
+type RefreshOptions = Omit<RedrawOptions, "signal">;
+
 export class Ddu {
   #loader: Loader;
   readonly #gatherStates = new Map<number, GatherState>();
@@ -76,7 +87,8 @@ export class Ddu {
   #aborter = new AbortController() as
     & Omit<AbortController, "abort">
     & GatherStateAbortable;
-  readonly #uiRedrawLock = new Lock(0);
+  readonly #uiRedrawLock: Lock<number>;
+  #waitCancelComplete = Promise.resolve();
   #waitRedrawComplete?: Promise<void>;
   #scheduledRedrawOptions?: Required<RedrawOptions>;
   #startTime = 0;
@@ -84,8 +96,13 @@ export class Ddu {
   #items: DduItem[] = [];
   readonly #expandedItems: Map<string, DduItem> = new Map();
 
-  constructor(loader: Loader) {
+  constructor(loader: Loader, uiRedrawLock: Lock<number>) {
     this.#loader = loader;
+    this.#uiRedrawLock = uiRedrawLock;
+  }
+
+  get cancelled(): AbortSignal {
+    return this.#aborter.signal;
   }
 
   async start(
@@ -93,7 +110,7 @@ export class Ddu {
     context: Context,
     options: DduOptions,
     userOptions: UserOptions,
-  ): Promise<unknown> {
+  ): Promise<void> {
     const prevContext = { ...this.#context };
     const { signal: prevSignal } = this.#aborter;
 
@@ -179,10 +196,7 @@ export class Ddu {
         // UI Redraw only
         // NOTE: Enable done to redraw UI properly
         this.#context.done = true;
-        await this.uiRedraw(
-          denops,
-          { signal: this.#aborter.signal },
-        );
+        await this.uiRedraw(denops);
         this.#context.doneUi = true;
         return;
       }
@@ -234,7 +248,7 @@ export class Ddu {
       .#createAvailableSourceStream(denops, { initialize: true })
       .tee();
     const [gatherStates] = availableSources
-      .pipeThrough(this.#createGatherStateTransformer(denops, signal))
+      .pipeThrough(this.#createGatherStateTransformer(denops))
       .tee();
 
     // Wait until initialized all sources. Source onInit() must be called before UI.
@@ -278,26 +292,30 @@ export class Ddu {
   async refresh(
     denops: Denops,
     refreshIndexes: number[] = [],
-  ): Promise<void> {
+    opts?: RefreshOptions,
+  ): Promise<AbortSignal> {
     this.#startTime = Date.now();
     this.#context.done = false;
 
     await this.cancelToRefresh(refreshIndexes);
+    this.#resetAborter();
 
     // NOTE: Get the signal after the aborter is reset.
     const { signal } = this.#aborter;
 
     // Initialize UI window
     if (this.#checkSync()) {
-      /* no await */ this.redraw(denops);
+      /* no await */ this.redraw(denops, { ...opts, signal });
     }
 
     const [gatherStates] = this
       .#createAvailableSourceStream(denops, { indexes: refreshIndexes })
-      .pipeThrough(this.#createGatherStateTransformer(denops, signal))
+      .pipeThrough(this.#createGatherStateTransformer(denops))
       .tee();
 
-    await this.#refreshSources(denops, gatherStates);
+    await this.#refreshSources(denops, gatherStates, { ...opts, signal });
+
+    return signal;
   }
 
   #createAvailableSourceStream(
@@ -354,7 +372,6 @@ export class Ddu {
 
   #createGatherStateTransformer(
     denops: Denops,
-    signal: AbortSignal,
   ): TransformStream<AvailableSourceInfo, GatherState> {
     return new TransformStream({
       transform: (sourceInfo, controller) => {
@@ -367,7 +384,6 @@ export class Ddu {
           sourceOptions,
           sourceParams,
           0,
-          { signal },
         );
         this.#gatherStates.set(sourceIndex, state);
 
@@ -379,9 +395,9 @@ export class Ddu {
   async #refreshSources(
     denops: Denops,
     gatherStates: ReadableStream<GatherState>,
-    opts?: { signal?: AbortSignal },
+    opts?: RedrawOptions,
   ): Promise<void> {
-    const { signal = this.#aborter.signal } = opts ?? {};
+    const redrawOpts = { signal: this.#aborter.signal, ...opts };
     const refreshErrorHandler = new AbortController();
     const refreshedSources: Promise<void>[] = [];
 
@@ -389,7 +405,7 @@ export class Ddu {
       new WritableStream({
         write: (state) => {
           refreshedSources.push(
-            this.#refreshItems(denops, state).catch((e) => {
+            this.#refreshItems(denops, state, redrawOpts).catch((e) => {
               refreshErrorHandler.abort(e);
             }),
           );
@@ -401,15 +417,21 @@ export class Ddu {
       { signal: refreshErrorHandler.signal },
     );
 
-    if (!this.#context.done) {
-      await this.redraw(denops, { signal });
+    if (redrawOpts.signal.aborted) {
+      // Redraw is aborted, so do nothing
+    } else if (!this.#context.done) {
+      await this.redraw(denops, redrawOpts);
     } else {
       await this.#waitRedrawComplete;
     }
   }
 
-  async #refreshItems(denops: Denops, state: GatherState): Promise<void> {
-    const { sourceInfo: { sourceOptions }, itemsStream, signal } = state;
+  async #refreshItems(
+    denops: Denops,
+    state: GatherState,
+    opts?: { signal?: AbortSignal },
+  ): Promise<void> {
+    const { sourceInfo: { sourceOptions }, itemsStream } = state;
 
     await callOnRefreshItemsHooks(
       denops,
@@ -432,7 +454,7 @@ export class Ddu {
       }
 
       if (this.#checkSync() && newItems.length > 0) {
-        /* no await */ this.redraw(denops, { signal });
+        /* no await */ this.redraw(denops, opts);
       }
     }
   }
@@ -484,10 +506,9 @@ export class Ddu {
     itemLevel: number,
     opts?: {
       parent?: DduItem;
-      signal?: AbortSignal;
     },
   ): GatherState<Params, UserData> {
-    const { parent, signal = this.#aborter.signal } = opts ?? {};
+    const { parent } = opts ?? {};
 
     const itemTransformer = new TransformStream<Item[], DduItem[]>({
       transform: (chunk, controller) => {
@@ -512,7 +533,6 @@ export class Ddu {
         sourceParams,
       },
       itemTransformer.readable,
-      { signal },
     );
 
     // Process from stream generation to termination.
@@ -532,7 +552,7 @@ export class Ddu {
         // Wait until the stream closes.
         await itemsStream.pipeTo(itemTransformer.writable);
       } catch (e: unknown) {
-        if (state.signal.aborted && e === state.signal.reason) {
+        if (state.cancelled.aborted && e === state.cancelled.reason) {
           // Aborted by signal, so do nothing.
         } else {
           // Show error message
@@ -547,9 +567,10 @@ export class Ddu {
   redraw(
     denops: Denops,
     opts?: RedrawOptions,
-  ): Promise<unknown> {
+  ): Promise<void> {
     const newOpts = {
       restoreItemState: false,
+      restoreTree: false,
       signal: this.#aborter.signal,
       ...opts,
     };
@@ -558,12 +579,10 @@ export class Ddu {
       // Already redrawing, so adding to schedule
       const prevOpts: RedrawOptions = this.#scheduledRedrawOptions ?? {};
       this.#scheduledRedrawOptions = {
+        ...newOpts,
         // Override with true
         restoreItemState: prevOpts.restoreItemState || newOpts.restoreItemState,
-        // Merge all signals
-        signal: prevOpts.signal && newOpts.signal !== prevOpts.signal
-          ? AbortSignal.any([newOpts.signal, prevOpts.signal])
-          : prevOpts.signal ?? newOpts.signal,
+        restoreTree: prevOpts.restoreTree || newOpts.restoreTree,
       };
     } else {
       // Start redraw
@@ -592,14 +611,13 @@ export class Ddu {
 
   async #redrawInternal(
     denops: Denops,
-    { restoreItemState, signal }: Required<RedrawOptions>,
+    { restoreItemState, restoreTree, signal }: Required<RedrawOptions>,
   ): Promise<void> {
     if (signal.aborted) {
       return;
     }
 
-    // Update current input
-    await this.setInput(denops, this.#input);
+    // Update current context
     this.#context.doneUi = false;
     this.#context.maxItems = 0;
 
@@ -753,6 +771,10 @@ export class Ddu {
       }
     }));
 
+    if (restoreTree) {
+      this.restoreTree(denops, { preventRedraw: true, signal });
+    }
+
     if (this.#context.done && this.#options.profile) {
       await printError(
         denops,
@@ -848,7 +870,9 @@ export class Ddu {
   quit() {
     // NOTE: quitted flag must be called after ui.quit().
     this.#quitted = true;
-    this.#aborter.abort({ reason: "quit" });
+    const reason = new QuitAbortReason();
+    this.#aborter.abort(reason);
+    /* no await */ this.#cancelGatherStates([], reason);
     this.#context.done = true;
   }
 
@@ -860,27 +884,32 @@ export class Ddu {
   async cancelToRefresh(
     refreshIndexes: number[] = [],
   ): Promise<void> {
-    this.#aborter.abort({ reason: "cancelToRefresh", refreshIndexes });
-
-    await Promise.all(
-      [...this.#gatherStates]
-        .map(([sourceIndex, state]) => {
-          if (isRefreshTarget(sourceIndex, refreshIndexes)) {
-            this.#gatherStates.delete(sourceIndex);
-            return state.waitDone;
-          }
-        }),
-    );
+    const reason = new RefreshAbortReason(refreshIndexes);
+    this.#aborter.abort(reason);
+    await this.#cancelGatherStates(refreshIndexes, reason);
+  }
 
-    this.#resetAborter();
+  #cancelGatherStates(
+    sourceIndexes: number[],
+    reason: GatherStateAbortReason,
+  ): Promise<void> {
+    const promises = [...this.#gatherStates]
+      .filter(([sourceIndex]) => isRefreshTarget(sourceIndex, sourceIndexes))
+      .map(([sourceIndex, state]) => {
+        this.#gatherStates.delete(sourceIndex);
+        state.cancel(reason);
+        return state.waitDone;
+      });
+    this.#waitCancelComplete = Promise.all([
+      this.#waitCancelComplete,
+      ...promises,
+    ]).then(() => {});
+    return this.#waitCancelComplete;
   }
 
   #resetAborter() {
     if (!this.#quitted && this.#aborter.signal.aborted) {
       this.#aborter = new AbortController();
-      for (const state of this.#gatherStates.values()) {
-        state.resetSignal(this.#aborter.signal);
-      }
     }
   }
 
@@ -1057,12 +1086,10 @@ export class Ddu {
       // Restore quitted flag before refresh and redraw
       this.#resetQuitted();
 
-      await this.refresh(denops);
-
-      if (searchPath.length <= 0) {
+      await this.refresh(denops, [], {
         // NOTE: If searchPath exists, expandItems() is executed.
-        await this.restoreTree(denops);
-      }
+        restoreTree: searchPath.length <= 0,
+      });
     } else if (uiOptions.persist || flags & ActionFlags.Persist) {
       // Restore quitted flag before refresh and redraw
       this.#resetQuitted();
@@ -1103,9 +1130,12 @@ export class Ddu {
   async expandItems(
     denops: Denops,
     items: ExpandItem[],
-    opts?: { signal?: AbortSignal },
+    opts?: {
+      preventRedraw?: boolean;
+      signal?: AbortSignal;
+    },
   ): Promise<void> {
-    const { signal = this.#aborter.signal } = opts ?? {};
+    const { preventRedraw, signal = this.#aborter.signal } = opts ?? {};
     for (const item of items.sort((a, b) => a.item.__level - b.item.__level)) {
       const maxLevel = item.maxLevel && item.maxLevel < 0
         ? -1
@@ -1124,7 +1154,9 @@ export class Ddu {
       );
     }
 
-    await this.uiRedraw(denops, { signal });
+    if (!preventRedraw && !signal.aborted) {
+      await this.uiRedraw(denops, { signal });
+    }
   }
 
   async expandItem(
@@ -1194,7 +1226,7 @@ export class Ddu {
         sourceOptions,
         sourceParams,
         parent.__level + 1,
-        { parent, signal },
+        { parent },
       );
 
       await state.readAll();
@@ -1719,6 +1751,10 @@ export class Ddu {
 
   async restoreTree(
     denops: Denops,
+    opts?: {
+      preventRedraw?: boolean;
+      signal?: AbortSignal;
+    },
   ): Promise<void> {
     // NOTE: Check expandedItems are exists in this.#items
     const checkItems: Map<string, DduItem> = new Map();
@@ -1734,7 +1770,7 @@ export class Ddu {
       return;
     }
 
-    await this.expandItems(denops, restoreItems);
+    await this.expandItems(denops, restoreItems, opts);
   }
 
   async #filterItems(
diff --git a/denops/ddu/ext.ts b/denops/ddu/ext.ts
index 5ba0e58..e551083 100644
--- a/denops/ddu/ext.ts
+++ b/denops/ddu/ext.ts
@@ -47,6 +47,7 @@ import type { BaseKind } from "./base/kind.ts";
 import type { BaseSource } from "./base/source.ts";
 import type { BaseUi } from "./base/ui.ts";
 import type { Loader } from "./loader.ts";
+import type { BaseAbortReason } from "./state.ts";
 import { convertUserString, printError } from "./utils.ts";
 
 import type { Denops } from "jsr:@denops/std@~7.4.0";
@@ -925,7 +926,7 @@ export async function uiRedraw<
     }
 
     try {
-      if (signal.aborted) {
+      if ((signal.reason as BaseAbortReason)?.type === "quit") {
         await ui.quit({
           denops,
           context,
@@ -953,7 +954,7 @@ export async function uiRedraw<
       });
 
       // NOTE: ddu may be quitted after redraw
-      if (signal.aborted) {
+      if ((signal.reason as BaseAbortReason)?.type === "quit") {
         await ui.quit({
           denops,
           context,
diff --git a/denops/ddu/state.ts b/denops/ddu/state.ts
index 0538e50..ba84f6d 100644
--- a/denops/ddu/state.ts
+++ b/denops/ddu/state.ts
@@ -1,9 +1,6 @@
 import type { BaseParams, DduItem, SourceOptions } from "./types.ts";
 import type { BaseSource } from "./base/source.ts";
 
-import { is } from "jsr:@core/unknownutil@~4.3.0/is";
-import { maybe } from "jsr:@core/unknownutil@~4.3.0/maybe";
-
 export type AvailableSourceInfo<
   Params extends BaseParams = BaseParams,
   UserData extends unknown = unknown,
@@ -14,14 +11,29 @@ export type AvailableSourceInfo<
   sourceParams: Params;
 };
 
-type GatherStateAbortReason =
-  | {
-    reason: "quit";
+export type BaseAbortReason = {
+  readonly type: string;
+};
+
+export class QuitAbortReason extends Error implements BaseAbortReason {
+  override name = "QuitAbortReason";
+  readonly type = "quit";
+}
+
+export class RefreshAbortReason extends Error implements BaseAbortReason {
+  override name = "RefreshAbortReason";
+  readonly type = "cancelToRefresh";
+  readonly refreshIndexes: readonly number[];
+
+  constructor(refreshIndexes: number[] = []) {
+    super();
+    this.refreshIndexes = refreshIndexes;
   }
-  | {
-    reason: "cancelToRefresh";
-    refreshIndexes: number[];
-  };
+}
+
+export type GatherStateAbortReason =
+  | QuitAbortReason
+  | RefreshAbortReason;
 
 export type GatherStateAbortable = {
   abort(reason: GatherStateAbortReason): void;
@@ -37,60 +49,15 @@ export class GatherState<
   #isDone = false;
   readonly #waitDone = Promise.withResolvers<void>();
   readonly #aborter = new AbortController();
-  #resetParentSignal?: AbortController;
 
   constructor(
     sourceInfo: AvailableSourceInfo<Params, UserData>,
     itemsStream: ReadableStream<DduItem[]>,
-    options?: {
-      signal?: AbortSignal;
-    },
   ) {
-    const { signal: parentSignal } = options ?? {};
     this.sourceInfo = sourceInfo;
-    this.#chainAbortSignal(parentSignal);
     this.itemsStream = this.#processItemsStream(itemsStream);
   }
 
-  resetSignal(signal?: AbortSignal): void {
-    // Do nothing if already aborted.
-    if (!this.#aborter.signal.aborted) {
-      this.#chainAbortSignal(signal);
-    }
-  }
-
-  #chainAbortSignal(parentSignal?: AbortSignal): void {
-    this.#resetParentSignal?.abort();
-    if (parentSignal == null) {
-      return;
-    }
-
-    const abortIfTarget = () => {
-      const reason = maybe(
-        parentSignal.reason,
-        is.ObjectOf({ reason: is.String }),
-      ) as GatherStateAbortReason | undefined;
-      if (
-        reason?.reason !== "cancelToRefresh" ||
-        isRefreshTarget(this.sourceInfo.sourceIndex, reason.refreshIndexes)
-      ) {
-        this.#aborter.abort(parentSignal.reason);
-      }
-    };
-
-    if (parentSignal.aborted) {
-      abortIfTarget();
-    } else {
-      this.#resetParentSignal = new AbortController();
-      parentSignal.addEventListener("abort", () => abortIfTarget(), {
-        signal: AbortSignal.any([
-          this.#aborter.signal,
-          this.#resetParentSignal.signal,
-        ]),
-      });
-    }
-  }
-
   #processItemsStream(
     itemsStream: ReadableStream<DduItem[]>,
   ): ReadableStream<DduItem[]> {
@@ -135,10 +102,14 @@ export class GatherState<
     return this.#waitDone.promise;
   }
 
-  get signal(): AbortSignal {
+  get cancelled(): AbortSignal {
     return this.#aborter.signal;
   }
 
+  cancel(reason?: unknown): void {
+    this.#aborter.abort(reason);
+  }
+
   async readAll(): Promise<void> {
     if (this.itemsStream != null) {
       await Array.fromAsync(this.itemsStream);