Filter a Tree by Substring and Render It Indented

intermediate
By FrontendAtlas Editorial · Updated Oct 14, 2026
Given an n-ary tree of { value, children } nodes and a needle string, drop every node whose value contains the needle together with its whole subtree, then render what remains as text: one line per node, depth-first in sibling order, indented two spaces per level. A public report of a senior frontend phone screen describes the same filter rendered as a UI tree; this practice version renders text so the tests can check order and indentation exactly. Concepts: tree, recursion, strings, immutability.

What you'll practice:

  • Pruning subtrees during a depth-first walk instead of filtering after the fact
  • Rendering tree depth as indentation without mutating the input
  • Defining the contract for a matching root, an empty needle and case sensitivity before coding

Expected behavior

Your renderFilteredTree() implementation should:

  • Return one line per surviving node, in depth-first order with siblings kept in their original order
  • Indent each line with two spaces per depth level and join lines with a newline, with no trailing newline
  • Drop a node and its entire subtree when node.value includes the needle (case-sensitive)
  • Return an empty string when the root itself matches
  • Prune nothing when the needle is an empty string
  • Leave the input tree unchanged

Arguments

  • root: { value: string; children?: TreeNode[] } — The root of an n-ary tree. children may be missing on leaves.
  • needle: string — The substring to prune by. A node whose value contains it disappears with its subtree.

Returns

string — The surviving tree rendered as indented text, or an empty string when the root matches.

Solution idea

Interview answer: decide the contract first. A node is pruned when its value includes the needle, and pruning a node also removes its subtree, so the check belongs at the moment you visit a node, not in a post-processing filter. Walk depth-first, carry the depth with each node, and emit one line per surviving node with two spaces per level. Treat the root like any other node (a matching root yields an empty string) and treat an empty needle as "keep everything", because every string includes the empty string. Build a fresh array of lines so the input tree is never mutated.

Examples
// Prune the 'offline' branch and render the rest
const fleet = {
  value: 'fleet',
  children: [
    { value: 'depot-east', children: [{ value: 'van-12' }, { value: 'van-13-offline' }] },
    { value: 'depot-west-offline', children: [{ value: 'van-21' }] },
  ],
};
renderFilteredTree(fleet, 'offline');
// => 'fleet\
  depot-east\
    van-12'

// A matching root removes everything
renderFilteredTree(fleet, 'fleet'); // => ''

// An empty needle keeps the whole tree
renderFilteredTree({ value: 'a', children: [{ value: 'b' }] }, ''); // => 'a\
  b'

Public interview reports

These public, self-reported interview accounts informed this practice version. The exercise is modelled on reported rounds, not copied from them, and the reports are not verified official questions or endorsements.