Y
Published on

BigFrontEnd Category 16 React Implementation Questions

Authors
  • avatar
    Name
    Yinhuan Yuan
    Twitter

Introduction

This blog post summarizes the React implementation related questions found on BigFrontEnd.Dev.

1.implement Immutability helper

12.https://bigfrontend.dev/problem/implement-Immutability-helper

If you use React, you would meet the scenario to copy the state for a slight change.

For example, for following state

const state = {
  a: {
    b: {
      c: 1,
    },
  },
  d: 2,
}

if we are to modify d to a new state, we could use _.cloneDeep, but it is not efficient because state.a is cloned while we don't need to change that.

A better way is to do shallow copy like this

const newState = {
  ...state,
  d: 3,
}

now is the problem, if we want to modify c, we would have to do something like

const newState = {
  ...state,
  a: {
    ...state.a,
    b: {
      ...state.b,
      c: 2,
    },
  },
}

We can see that for simple data structure it would be enough to use spread operator, but for complex data structures, it is verbose.

Here comes the Immutability Helper, you are asked to implement your own Immutability Helper update(), which supports following features.

  1. {$push: array} push() all the items in array on the target.
const arr = [1, 2, 3, 4]
const newArr = update(arr, {$push: [5, 6]})
  1. {$set: any} replace the target
const state = {
  a: {
    b: {
      c: 1,
    },
  },
  d: 2,
}
const newState = update(state, { a: { b: { c: { $set: 3 } } } })
/*
{
  a: {
    b: {
      c: 3
    }
  },
  d: 2
}
*/

Notice that we could also update array elements with $set

const arr = [1, 2, 3, 4]
const newArr = update(arr, { 0: { $set: 0 } })
  1. {$merge: object} merge object to the location
const state = {
  a: {
    b: {
      c: 1,
    },
  },
  d: 2,
}
const newState = update(state, { a: { b: { $merge: { e: 5 } } } })
/*
{
  a: {
    b: {
      c: 1,
      e: 5
    }
  },
  d: 2
}
*/
  1. {$apply: function} custom replacer
const arr = [1, 2, 3, 4]
const newArr = update(arr, { 0: { $apply: (item) => item * 2 } })

Solution:

Firstly, we need to verify whether the command is object and is not null. Then, we test whether data is array. If it is an array, we use a function named handleArray to process it. Otherwise, we use another function named handleObject to process it.

/**
 * @param {any} data
 * @param {Object} command
 */
function update(data, command) {
  if (typeof command !== 'object' || command === null) {
    throw new Error('Spec should be a non-null object')
  }

  if (Array.isArray(data)) {
    return handleArray(data, command)
  }

  return handleObject(data, command)
}
// $push, $set, $apply
function handleArray(target, spec) {
  let newArray = target.slice()
  for (let key in spec) {
    // The property may exist directly on the object or is inherited from its prototype chain.
    // We only want to deal with the former.
    if (!spec.hasOwnProperty(key)) continue
    if (key === '$push') {
      newArray.push(...spec[key])
    } else if (key.match(/^\d+$/)) {
      const index = parseInt(key, 10)
      if (spec[key].$set !== undefined) {
        newArray[index] = spec[key].$set
      } else if (spec[key].$apply !== undefined) {
        newArray[index] = spec[key].$apply(newArray[index])
      } else {
        newArray[index] = update(newArray[index], spec[key])
      }
    } else {
      throw new Error(`Invalid key for array update: ${key}`)
    }
  }
  return newArray
}
// $merge, $set, $apply
function handleObject(target, spec) {
  let newObject = { ...target }

  for (let key in spec) {
    if (!spec.hasOwnProperty(key)) continue
    if (key === '$set') {
      return spec[key]
    } else if (key === '$merge') {
      if (Object.keys(newObject).length === 0) {
        throw new Error(`Invalid spec`)
      }
      Object.assign(newObject, spec[key])
    } else if (key === '$apply') {
      return spec[key](newObject)
    } else {
      newObject[key] = update(newObject[key], spec[key])
    }
  }
  return newObject
}

2.find corresponding node in two identical DOM tree

19.https://bigfrontend.dev/problem/find-corresponding-node-in-two-identical-DOM-tree

Given two same DOM tree A, B, and an Element a in A, find the corresponding Element b in B.

By corresponding, we mean a and b have the same relative position to their DOM tree root.

follow up

This could be a problem on general Tree structure with only children.

Could you solve it recursively and iteratively?

Could you solve this problem with special DOM api for better performance?

What are the time cost for each solution?

Solution:

/**
 * @param {HTMLElement} rootA
 * @param {HTMLElement} rootB - rootA and rootB are clone of each other
 * @param {HTMLElement} nodeA
 */
function findCorrespondingNode(rootA, rootB, targetA) {
  // Stack to store nodes to visit
  const stackA = [rootA]
  const stackB = [rootB]

  while (stackA.length > 0) {
    const currentA = stackA.pop()
    const currentB = stackB.pop()

    // If we find the target in A, return the corresponding node in B
    if (currentA === targetA) {
      return currentB
    }

    // Push children to the stacks
    stackA.push(...currentA.children)
    stackB.push(...currentB.children)
  }

  return null
}
function dfsTraverse(root, callback) {
  if (root === null || root === undefined) {
    return
  }
  dfsTraverse(root.left, callback)
  callback(root)
  dfsTraverse(root.right, callback)
}

3.Virtual DOM I

113.https://bigfrontend.dev/problem/Virtual-DOM-I

Suppose you have solved 110. serialize and deserialize binary tree, have you wondered how to do similar task to DOM tree ?

HTML string could be thought as some sort of serialization, the browser parses(deserialize) the HTML → construct the DOM tree.

Besides XML base, we could try JSON for this. If we log the element presentation in React, like below

const el = (
  <div>
    <h1> this is </h1>
    <p className="paragraph">
      {' '}
      a <button> button </button> from{' '}
      <a href="https://bfe.dev">
        <b>BFE</b>.dev
      </a>
    </p>
  </div>
)
console.log(el)

we would get this( ref, key .etc are stripped off)

{
  type: 'div',
  props: {
    children: [
      {
        type: 'h1',
        props: {
          children: ' this is '
        }
      },
      {
        type: 'p',
        props: {
          className: 'paragraph',
          children: [
            ' a ',
            {
              type: 'button',
              props: {
                children: ' button '
              }
            },
            ' from',
            {
              type: 'a',
              props: {
                href: 'https://bfe.dev',
                children: [
                  {
                    type: 'b',
                    props: {
                      children: 'BFE'
                    }
                  },
                  '.dev'
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

Clearly this is the same tree structure but only in object literal.

Now you are asked to serialize/deserialize the DOM tree, like what React does.

Note

Functions like event handlers and custom components are beyond the scope of this problem, you can ignore them, just focus on basic HTML tags.

You should support:

  1. TextNode (string) as children
  2. single child and multiple children
  3. camelCased properties.

virtualize() takes in a real DOM tree and create an object literal. render() takes in a object literal presentation and recreate a DOM tree.

Solution:

/**
 * @param {HTMLElement}
 * @return {object} object literal presentation
 */
function virtualize(element) {
  if (element.nodeType === Node.TEXT_NODE) return element.nodeValue

  const result = {
    type: element.nodeName.toLowerCase(),
    props: {},
  }

  for (const attr of element.attributes) {
    const name = attr.nodeName === 'class' ? 'className' : attr.nodeName
    result.props[name] = attr.nodeValue
  }

  const children = [...element.childNodes].map(virtualize)
  result.props.children = children.length === 1 ? children[0] : children
  return result
}

/**
 * @param {object} valid object literal presentation
 * @return {HTMLElement}
 */
function render(obj) {
  if (typeof obj === 'string') {
    return document.createTextNode(obj)
  }

  const {
    type,
    props: { children, ...attrs },
  } = obj
  const element = document.createElement(type)

  for (const attr in attrs) {
    const attrName = attr === 'className' ? 'class' : attr
    element.setAttribute(attrName, attrs[attr])
  }

  if (typeof children === 'string') {
    element.appendChild(render(children))
    return element
  }

  for (const child of children) {
    element.appendChild(render(child))
  }

  return element
}

4.Virtual DOM II - createElement

118.https://bigfrontend.dev/problem/virtual-dom-II-createElement

This is a follow-up on 113. Virtual DOM I.

Suppose you have solved above problem, now let's take a look at React.createElement():

React.createElement(type, [props], [...children])

First argument is type, it could be set to Custom Component, but here in this problem, it would only be HTML tag name Second argument is props, here in this problem, it would only be the (common) camelCased HTML attributes the rest arguments are the children, which in React supports many data types, but in this problem, it only has the element type of MyElement, or string for TextNode. You are asked to create your own createElement() and render(), so that following code could create the exact HTMLElement in 113. Virtual DOM I.

const h = createElement
render(
  h(
    'div',
    {},
    h('h1', {}, ' this is '),
    h(
      'p',
      { className: 'paragraph' },
      ' a ',
      h('button', {}, ' button '),
      ' from ',
      h('a', { href: 'https://bfe.dev' }, h('b', {}, 'BFE'), '.dev')
    )
  )
)

Notes

The goal of this problem is not to create the replica of React implementation, you can have your own object representation format other than the one in 113. Virtual DOM I.

Details about ref, key are ignored here, they will be put in other problems. Re-render is not covered here, it will be in another problem as well.

Solution:

/**
 * MyElement is the type your implementation supports
 *
 * type MyNode = MyElement | string
 */

/**
 * @param { string } type - valid HTML tag name
 * @param { object } [props] - properties.
 * @param { ...MyNode} [children] - elements as rest arguments
 * @return { MyElement }
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}

/**
 * @param { MyElement }
 * @returns { HTMLElement }
 */
function render(myElement) {
  if (typeof myElement === "string") {
    return document.createTextNode(myElement);
  }

  const {
    type,
    props: { children, ...attrs },
  } = myElement;
  const element = document.createElement(type);

  for (const attrName in attrs) {
    const _attrName = attrName === "className" ? "class" : attrName;
    element.setAttribute(_attrName, attrs[attrName]);
  }

  for (const child of children) {
    element.appendChild(render(child));
  }

  return element;

5.Virtual DOM III - Functional Component

140.https://bigfrontend.dev/problem/virtual-DOM-III-Functional-Component

This is a follow-up on 118. Virtual DOM II - createElement.

In problem 118, you are asked to implement createElement() and render() function which supports intrinsic HTML elements, like <p/>, <div/> etc.

In this problem, you are ask to support custom Functional Component.

Functional Component are functions that:

  1. accept single object argument -props, which contains children, className and other properties.
  2. returns an MyElement by calling createElement(). Say we have a Functional Component - Title
const h = createElement
const Title = ({ children, ...res }) => h('h1', res, ...children)

Then we should be able to use it in createElement and render(), just the same way as an intrinsic element.

h(Title, {}, 'This is a title')
h(Title, { className: 'class1' }, 'This is a title')

Please modify your createElement() and render() from 118. Virtual DOM II - createElement if necessary, so that the example in problem 118 could be rewritten as below:

const Link = ({ children, ...res }) => h('a', res, ...children)
const Name = ({ children, ...res }) => h('b', res, ...children)
const Button = ({ children, ...res }) => h('button', res, ...children)
const Paragraph = ({ children, ...res }) => h('p', res, ...children)
const Container = ({ children, ...res }) => h('div', res, ...children)
h(
  Container,
  {},
  h(Title, {}, ' this is '),
  h(
    Paragraph,
    { className: 'paragraph' },
    ' a ',
    h(Button, {}, ' button '),
    ' from ',
    h(Link, { href: 'https://bfe.dev' }, h(Name, {}, 'BFE'), '.dev')
  )
)

Solution:

We can treat all types of elements as FunctionComponent. During rendering, we can check if the element's type is a function. If it is, we call the function to create the element, then invoke the render function again to convert it into an HTML element.

/**
 * MyElement is the type your implementation supports
 *
 * type MyNode = MyElement | string
 * type FunctionComponent = (props: object) => MyElement
 */

/**
 * @param { string | FunctionComponent } type - valid HTML tag name or Function Component
 * @param { object } [props] - properties.
 * @param { ...MyNode} [children] - elements as rest arguments
 * @return { MyElement }
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}

/**
 * @param { MyElement }
 * @returns { HTMLElement }
 */
function render(myElement) {
  if (typeof myElement === "string") {
    return document.createTextNode(myElement);
  }

  const {
    type,
    props,
  } = myElement;

  if (typeof type === "function") {
    return render(type(props));
  }
  /*
  const {
    type,
    props: { children, ...attrs },
  } = myElement;
  */
  const { children, ...attrs } = props;
  const element = document.createElement(type);

  for (const attrName in attrs) {
    const _attrName = attrName === "className" ? "class" : attrName;
    element.setAttribute(_attrName, attrs[attrName]);
  }

  for (const child of children) {
    element.appendChild(render(child));
  }

  return element;
}

6.Virtual DOM IV - JSX 1

143.https://bigfrontend.dev/problem/virtual-dom-iv-jsx-1

If you are using React, you must be familiar with JSX.

With JSX syntax support, transpilers are able to understand below non-standard code directly in JavaScript.

<p>this is <button className="button">button</button></p>

Then it is transpiled to standard JavaScript function calls.

React.createElement(
  'p',
  null,
  ' this is ',
  React.createElement('button', { className: 'button' }, 'button'),
  ' '
)

have a try at TypeScript Playground

To illustrate how the transpilation works, let's start with a simple example.

<a>bfe.dev</a>

First the parser will create an AST(Abstract Syntax Tree) from the code.

Open above code in AST Explorer, you can see the AST in the right pannel, roughly something like this:

expression: JSXElement {
  openingElement: JSXOpeningElement {
    name: JSXIdentifier {
      name: "a"
    }
  }
  closingElement: JSXClosingElement {
    name: JSXIdentifier {
      name: "a"
    }
  }
  children: [
    JSXText {
      value: "bfe.dev"
    }
  ]
}

Obviously above AST follows the JSX Spec:

JSXElement:
  JSXOpeningElement JSXChildren? JSXClosingElement

JSXOpeningElement:
  < JSXElementName JSXAttributes? >

JSXChildren:
  JSXChild JSXChildren?

JSXClosingElement:
  < / JSXElementName >

JSXChild:
  JSXText
  JSXElement
  { JSXChildExpression? }

With the above AST, it is fairly easy to generate code, we only need to traverse the AST and insert React.createElement:

React.createElement(
  'p',
  null,
  ' this is ',
  React.createElement('button', { className: 'button' }, 'button'),
  ' '
)

Also instead of React method, we could use h() defined in 140. Virtual DOM III - Functional Component instead.

h('p', null, ' this is ', h('button', { className: 'button' }, 'button'), ' ')

Now, please create your own parse() and generate() to transpile JSX Element code.

  1. please generate code which uses h(), h() is bundled with your code.
  2. Goal of this problem is not to recreate the full parser, so only need to support the minumum spec below:
JSXElement:
  JSXOpeningElement JSXChildren? JSXClosingElement
JSXOpeningElement:
  < JSXElementName >
JSXChildren:
  JSXChild
JSXClosingElement:
  < / JSXElementName >
JSXChild:
  JSXText
  • you can choose not to follow the naming
  • there is no newlines in the input, you can ignore the whitespace rules
  • all input tags are smallcase HTML tags
  1. for simplicity, the AST creating process with parse() won't be tested, rather parse() and generate() are tested together like this:
const result = eval(generate(parse('<a>bfe.dev</a>')))
expect(result).toEqual(h('a', null, 'bfe.dev'))
  1. An error should be thrown if code is not valid JSXElement, for example, the JSXOpeningElement and JSXClosingElement might not be matched. The test cases only cover some of the common errors.

Solution:

A regular expression: const regex = /^\s*<\s*(\w+)\s*>([^<>]*)<\s*\/\s*(\w+)\s*>\s*$/; is designed to match and capture parts of an XML-like tag structure. Let's break it down piece by piece:

  1. /^ ... $/: The ^ and $ indicate that the pattern must match the entire string from start to end.

  2. \s*: Matches zero or more whitespace characters. This appears multiple times in the regex to allow for optional spacing.

  3. <: Matches the opening angle bracket of the tag.

  4. (\w+): Captures one or more word characters (letters, digits, or underscores). This captures the tag name.

  5. >: Matches the closing angle bracket of the opening tag.

  6. ([^<>]*): Captures any characters that are not angle brackets. This captures the content between the tags.

  7. <\/: Matches the opening of the closing tag, including the forward slash.

  8. (\w+): Captures the closing tag name.

  9. >: Matches the final closing angle bracket.

Here's a visual breakdown:

/^\s*<\s*(\w+)\s*>([^<>]*)<\s*\/\s*(\w+)\s*>\s*$/
  |  |  |   |    | |     | |   |   |    |  | |
  |  |  |   |    | |     | |   |   |    |  | End of string
  |  |  |   |    | |     | |   |   |    |  Optional whitespace
  |  |  |   |    | |     | |   |   |    Closing tag name
  |  |  |   |    | |     | |   |   Optional whitespace
  |  |  |   |    | |     | |   Forward slash
  |  |  |   |    | |     | |   Optional whitespace
  |  |  |   |    | |     | Opening of closing tag
  |  |  |   |    | |     Content between tags
  |  |  |   |    | Closing angle bracket
  |  |  |   |    Optional whitespace
  |  |  |   Opening tag name
  |  |  Opening angle bracket
  |  Optional whitespace
  Start of string

This regex will match strings like:

  • <tag>content</tag>
  • <tag>content</tag>
  • <tag >content< / tag >

It will capture:

  1. The opening tag name
  2. The content between tags
  3. The closing tag name

For example, if applied to <hello>world</hello>, it would capture:

  1. "hello"
  2. "world"
  3. "hello"

This regex is strict in that it requires the opening and closing tags to match and doesn't allow for attributes in the opening tag. It's a basic pattern for simple XML-like structures.

/**
 * @param {code} string
 * @returns {any} AST
 */
function parse(code) {
   const regex = /^\s*<\s*(\w+)\s*>([^<>]*)<\s*\/\s*(\w+)\s*>\s*$/;
   const match = code.match(regex);
   if (!match) {
     throw new Error();
   }

   const openingEl = match[1];
   const closingEl = match[3];
   if (openingEl !== closingEl) {
     throw new Error();
   }

   return {
     openingElement: {
       name: openingEl,
     },
     closingElement: {
       name: closingEl,
     },
     children: match[2] ? [match[2]] : [],
   };
}

/**
 * @param {any} your AST
 * @returns {string}
 */
 function generate(ast) {
   return {
     type: ast.openingElement.name,
     props: {
       children: ast.children,
     },
   };
}

Solution 2:

This is another solution without using regular expression. We divied the parse element into three steps:

  1. parse Open Tag:
  2. parse children:
  3. parse Close Tag:

We also design a expect and trimSpaces to identify the tag name and removing spaces.

/**
 * @param {code} string
 * @returns {any} AST
 */
function parse(code) {
  let index = 0;

  function parseElement() {
    const openTag = parseOpenTag();
    const children = parseChildren();
    const closeTag = parseCloseTag();
    trimSpaces();
    if (index < code.length) {
      throw new Error('Must reach the end of string');
    }
    if (openTag.name !== closeTag.name) {
      throw new Error('Opening and closing tags do not match');
    }

    return {
      type: 'JSXElement',
      openingElement: openTag,
      children: children,
      closingElement: closeTag
    };
  }

  function parseOpenTag() {
    expect('<');
    const name = parseName();
    expect('>');
    return { type: 'JSXOpeningElement', name };
  }

  function parseCloseTag() {
    expect('<');
    expect('/');
    const name = parseName();
    expect('>');
    return { type: 'JSXClosingElement', name };
  }

  function parseChildren() {
    const children = [];
    while (index < code.length && code[index] !== '<') {
      children.push({ type: 'JSXText', value: parseText() });
    }
    return children;
  }

  function parseName() {
    trimSpaces();
    let name = '';
    while (index < code.length && /[a-z]/.test(code[index])) {
      name += code[index++];
    }
    if (name === '') throw new Error('Expected tag name');
    return name;
  }

  function parseText() {
    let text = '';
    while (index < code.length && code[index] !== '<') {
      text += code[index++];
      if (text.at(-1) === ">") {
        throw new Error("> should be included as text.");
      }
    }
    return text;
  }

  function expect(char) {
    trimSpaces()
    if (code[index] !== char) {
      throw new Error(`Expected ${char}, found ${code[index]}`);
    }
    index++;
  }

  function trimSpaces() {
    while (code[index] === ' ') {
      index++
    }
  }

  return parseElement();
}

/**
 * @param {any} your AST
 * @returns {string}
 */
 function generate(ast) {
   return {
     type: ast.openingElement.name,
     props: {
       children: ast.children.map(child => child.value),
     },
   };
}

7.Virtual DOM V - JSX 2

150.https://bigfrontend.dev/problem/virtual-dom-v-jsx-2

This is a follow-up on 143. Virtual DOM IV - JSX 1.

Congratulations on your pass on problem 143!

Now in this problem, please modify your code to support following.

  1. nesting elements
<p><i>BFE.dev</i> is <b>cool</b>!</p>

This means JSXChild needs to support JSXElement as well.

JSXChild: JSXText + JSXElement
  1. Functional Component As a convention, intrinsic HTML tags are lower cases and Functional Components have capitalized initials.
const Heading = ({children, ...res}) => h('h1', res, ...children)

<Heading>BFE.<i>dev</i></Heading>

If your code in problem 143 already supports this, that's fantastic 👍! Just copy your code here and hope it shall pass.

Solution:

This solution is still failed because of Functional components.

/**
 * @param {code} string
 * @returns {any} AST
 */
function parse(code) {
  let index = 0

  function parseElement() {
    const openTag = parseOpenTag()
    const children = parseChildren()
    const closeTag = parseCloseTag()
    if (openTag.name !== closeTag.name) {
      throw new Error('Opening and closing tags do not match')
    }

    return {
      type: 'JSXElement',
      openingElement: openTag,
      children: children,
      closingElement: closeTag,
    }
  }

  function parseOpenTag() {
    expect('<')
    const name = parseName()
    expect('>')
    return { type: 'JSXOpeningElement', name }
  }

  function parseCloseTag() {
    expect('<')
    expect('/')
    const name = parseName()
    expect('>')
    return { type: 'JSXClosingElement', name }
  }

  function parseChildren() {
    const children = []
    while (index < code.length) {
      if (code[index] === '<') {
        let j = index + 1
        while (code[j] === ' ') {
          j++
        }
        if (code[j] === '/') {
          break
        }
        children.push(parseElement())
      } else {
        while (index < code.length && code[index] !== '<') {
          const value = parseText();
            children.push({ type: 'JSXText', value})
        }
      }
    }
    return children
  }

  function parseName() {
    trimSpaces()
    let name = ''
    while (index < code.length && /[a-z]|[A-Z]/.test(code[index])) {
      name += code[index++]
    }
    if (name === '') throw new Error('Expected tag name')
    return name
  }

  function parseText() {
    let text = ''
    while (index < code.length && code[index] !== '<') {
      text += code[index++]
      if (text.at(-1) === '>') {
        throw new Error('> should be included as text.')
      }
    }
    return text
  }

  function expect(char) {
    trimSpaces()
    if (code[index] !== char) {
      throw new Error(`Expected ${char}, found ${code[index]}`)
    }
    index++
  }

  function trimSpaces() {
    while (code[index] === ' ') {
      index++
    }
  }
  const result = parseElement()
  trimSpaces();
  if (index < code.length) {
    throw new Error('Must reach the end of string');
  }
  return result
}

/**
 * @param {any} your AST
 * @returns {string}
 */
function generate(ast) {
  const isFunctionComponent = /[A-Z]/.test(ast.openingElement.name[0]);
  const functionName = ast.openingElement.name;
  /*
  const myFunctions = {
    [functionName]: function() {
        console.log("This is a custom function with a variable name!");
    }
  };
  */
  return {
    type: isFunctionComponent ? functionName : ast.openingElement.name,
    props: {
      children: ast.children.map((child) => {
        if (child.type === 'JSXText') {
          return child.value
        }
        if (child.type === 'JSXElement') {
          return generate(child)
        }
      }),
    },
  }
}

8.implement classNames()

125.https://bigfrontend.dev/problem/implement-classnames

If using React, you can set the prop className to add class name to an element, it is string so you can add multiple class names like this:

<p className="classname1 classname2">BFE.dev can help</p>

When class names are dynamically generated, it becomes verbose.

<p className={`classname1  ${shouldAddClassname2 ? 'classname2' : ''}`}>BFE.dev can help</p>

classnames can help with this.

classNames() accepts arbitrary arguments, filter out the falsy values, and generate the final className string.

1.string and number are used directly.

classNames('BFE', 'dev', 100)
// 'BFE dev 100'

2.other primitives are ignored.

classNames(null, undefined, Symbol(), 1n, true, false)
// ''

3.Object's enumerable property keys are kept if the key is string and value is truthy. Array should be flattened.

const obj = new Map()
obj.cool = '!'
classNames({ BFE: [], dev: true, is: 3 }, obj)
// 'BFE dev is cool'
classNames(['BFE', [{ dev: true }, ['is', [obj]]]])
// 'BFE dev is cool'

Please implement your own classNames(). note

It is not the goal to reimplement the original package, so the spec might be a little different, please follow the above description.

Solution:

/**
 * @param {any[]} args
 * @returns {string}
 */
function classNames(...args) {
  const convertArg = (arg) => {
    if (typeof arg === 'number' || typeof arg === 'string') {
      return String(arg)
    }
    if (Array.isArray(arg)) {
      return arg.map(convertArg).join(' ')
    }
    return Object.keys(arg)
      .filter((key) => arg[key])
      .join(' ')
  }
  args = args.filter(
    (arg) =>
      typeof arg === 'number' ||
      typeof arg === 'string' ||
      (typeof arg === 'object' && arg !== null)
  )
  return args.map(convertArg).join(' ')
}
/**
 * @param {any[]} args
 * @returns {string}
 */
function classNames(...args) {
  const result = []

  function processValue(value) {
    if (typeof value === 'string' || typeof value === 'number') {
      result.push(value)
    } else if (Array.isArray(value)) {
      value.forEach(processValue)
    } else if (typeof value === 'object' && value !== null) {
      for (const key in value) {
        if (value.hasOwnProperty(key) && value[key]) {
          result.push(key)
        }
      }
    }
  }

  args.forEach(processValue)

  return result.join(' ')
}

9.lit-html 1 - tagged templates

142.https://bigfrontend.dev/problem/lit-html-1-tagged-templates

According to lit-html homepage,

lit-html lets you write HTML templates in JavaScript, then efficiently render and re-render those templates together with data to create and update DOM

This video explains it pretty well about how it works. Let's take a look at the example.

import { html, render } from 'lit-html'
const helloTemplate = (name) => html`<div>Hello ${name}!</div>`
// This renders <div>Hello Steve!</div> to the document body
render(helloTemplate('Steve'), document.body)
// This updates to <div>Hello Kevin!</div>, but only updates the ${name} part
render(helloTemplate('Kevin'), document.body)

The magic happens in the second call of render() which only updates the necessary parts.

But there will be a series of problems on BFE.dev leading to that, here you are asked to :

implement html() and render() to make above example work, without considering the rerender, so html() could just return the raw HTML string.

The input data are all valid.

Solution:

Here is the explanation for these functions.

  1. html function:

    • This function uses tagged template literals.
    • It takes two parameters: an array of string literals (strings) and the rest of the arguments as values (...values).
    • It uses reduce to combine the strings and interpolated values.
    • The result is a single string that represents the HTML template with interpolated values.
  2. render function:

    • This function takes two parameters: the template (which is a string in this basic implementation) and the container DOM element.
    • It simply sets the innerHTML of the container to the template string.
function html(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] || '');
  }, '');
}

function render(template, container) {
  container.innerHTML = template;
}

10.CountdownTimer

Please implement a countdown timer with initial value 10 and three buttons: Start, Stop, and Reset. When the Start button is clicked, the counting down will start by decreasing 1. When it reach 0, it will go back to 10. When the Stop button is clicked, the counting down is stopped. When the Reset button is clicked, the counting down is stopped and the counting down value is reset to 10.

Solution:

We can use useEffect to wrap the logic to synchronize side effects (like setting up intervals) with React's rendering cycle. It ensures that our interval logic runs after the component has rendered and updates properly when relevant state changes.

import React, { useState, useEffect } from 'react'

const CountdownTimer = () => {
  const [count, setCount] = useState(10)
  const [isRunning, setIsRunning] = useState(false)

  useEffect(() => {
    let interval

    if (isRunning) {
      interval = setInterval(() => {
        setCount((prevCount) => {
          if (prevCount === 0) {
            return 10
          }
          return prevCount - 1
        })
      }, 1000)
    }

    return () => clearInterval(interval)
  }, [isRunning, count])

  const handleStart = () => {
    setIsRunning(true)
  }

  const handleStop = () => {
    setIsRunning(false)
  }

  const handleReset = () => {
    setIsRunning(false)
    setCount(10)
  }

  return (
    <div>
      <div>
        <button onClick={handleStart}>Start</button>
        <button onClick={handleStop}>Stop</button>
        <button onClick={handleReset}>Reset</button>
      </div>
      <div>
        <p>Count: {count}</p>
      </div>
    </div>
  )
}

export default CountdownTimer

11.Auto-focus an input in React

Create a React compoent with an input. The input will be focused in the initial loading.

import {useRef, useEffect} from 'react';
export default function Component () {
  const inputRef = useRef(null);
  useEffect(() => inputRef.current.focus(), []);
  return <input type="email" ref={inputRef} />
}