This article shares two simple examples of developing with React Hook and functional components.

A simple component example

The Button component is probably the simplest common basic component. When we develop a component, we expect it to have a degree of variation in its basic style so that it can be used in different scenarios. The second point is that I wrote a function component when I was doing the project before, but the function component was very rigid, that is, there was no way to bind the basic method. I can only write methods or features that I already have. I want to write a Button component, and even if I don’t write an onClick method, I want to be able to use the default base methods that come with it.

For the first point, it’s easier to write different CSS for different classnames.

The second is slightly harder to achieve. We can’t write all of Button’s default properties. It would be nice if we could import all of the default properties.

In fact, React already does this for us. The React. ButtonHTMLAttributes < HTMLElement > inside contains the default Button properties. However, we can’t use this interface directly because our Button component might have some customization. For this, we can use Typescript cross types

type NativeButtonProps = MyButtonProps & React.ButtonHTMLAttributes<HTMLElement>
Copy the code

In addition, we need to use resProps to import other non-custom functions or properties.

The following is the specific implementation scheme of Button component:

import React from 'react'
import classNames from 'classnames'

type ButtonSize = 'large' | 'small'
type ButtonType = 'primary' | 'default' | 'danger'

interfaceBaseButtonProps { className? :string; disabled? :boolean; size? : ButtonSize; btnType? : ButtonType; children? : React.ReactNode; }type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>
const Button: React.FC<NativeButtonProps>= (props) = > {
  const {
    btnType,
    className,
    disabled,
    size,
    children,
    // resProps is used to extract all remaining properties. resProps } = props// btn, btn-lg, btn-primary
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': disabled
  })
  return (
    <button
      className={classes}
      disabled={disabled}
      {. resProps}
    >
      {children}
    </button>
  )
}

Button.defaultProps = {
  disabled: false.btnType: 'default'
}

export default Button
Copy the code

This way, we can use methods such as onClick in our custom Button component. Use Button components as follows:

<Button disabled>Hello</Button>
<Button btnType='primary' size='large' className="haha">Hello</Button>
<Button btnType='danger' size='small' onClick={()= > alert('haha')}>Test</Button>
Copy the code

The display effect is as follows:

In this code we have introduced a new NPM package called classNames. For details, please refer to GitHub classNames. It is easy to implement the extension of className.

classNames('foo'.'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); / / = > '
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true.bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames('foo', { bar: true.duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null.false.'bar'.undefined.0.1, { baz: null }, ' '); // => 'bar 1'
Copy the code

Using classNames makes it easy to add personalized attributes to a Button. You can see hahaclassName in the HTML output for the component:

<button class="btn haha btn-primary btn-lg">Hello</button>
Copy the code

At the same time, our code approach solves the problem that custom components cannot use default properties and methods.

More complex parent-child component cases

Next we show how to use function components to accomplish a menu function. This menu adds two functional modes, horizontal mode and vertical mode. Click on a menu detail as a child component.

Of course, the menu function does not require the parent component to pass data to the child component (the child component refers to the menu details), so we forced it to be added in order to learn and demonstrate how to pass the parent component data to the child component. A little gilding the lily, just so you understand.

This section describes the functions of parent and child components. Menu is the overall parent component, MenuItem is each specific small Menu, SubMenu is a drop-down Menu that can be opened.

Here’s what it looks like when expanded:

The overall code structure is as follows:

<Menu defaultIndex={'0'} onSelect={(index)= > {alert(index)}} mode="vertical" defaultOpenSubMenus={['2']}>
  <MenuItem index={'0'} >
    cool link
  </MenuItem>
  <MenuItem index={'1'} >
    cool link 2
  </MenuItem>
  <SubMenu title="dropdown">
    <MenuItem index={'3'} >
      dropdown 1
    </MenuItem>
    <MenuItem index={'4'} >
      dropdown 2
    </MenuItem>
  </SubMenu>
  <MenuItem index={'2'} >
    cool link 3
  </MenuItem>
</Menu>
Copy the code

In this component, we use useState, and useContext because it involves passing data from the parent to the child (passing data from the parent to the child means passing index data from the parent to the child). In addition, we’ll demonstrate the use of custom onSelect to implement the onClick function (in case you don’t succeed in introducing React generics, or don’t know which React generics to introduce, you can use custom ones as a remedy).

How to write the onSelect

In order to prevent later in the code in the sea of difficult to find onSelect, here is a simple out to do an onSelect writing example. For example, if we use onSelect in the Menu component, it looks the same as onClick:

<Menu onSelect={(index)= > {alert(index)}}>
Copy the code

In the specific Menu component, the specific use of onSelect can be written as follows:

type SelectCallback = (selectedIndex: string) = > void

interfaceMenuProps { onSelect? : SelectCallback; }Copy the code

The implementation of handleClick can be written like this:

  const handleClick = (index: string) = > {
    // onSelect is a union type. It may or may not exist
    if (onSelect) {
      onSelect(index)
    }
  }
Copy the code

When passing the onSelect to a child component, use the onSelect: handleClick binding. (Maybe you didn’t understand it very well, and I don’t know how to write it. There will be an overall code analysis later, and it may be easier to understand if you read it together.)

React.Children

Before I get into the code, there are a few more tips, one of which is react.children.

React.Children provides a practical method for dealing with opaque data structures for this.props. Children.

For details, see React.children.map

Why do we need to use React.Children? This is because if the parent component data is passed to the child component, it may need to be traversed or further processed by the child component. But we can’t guarantee whether there are any, one, two, or more child components.

The value of this.props. Children has three possibilities: it is undefined if the current component has no children; If there is a child node, the data type is Object; If there are multiple child nodes, the data type is array. So be careful with this.props. Children [1].

React provides a utility method to handle this.props. Children. We can iterate over the Children with react.children. map without worrying about whether this. Props.Children is undefined or object[1].

So, if we have a parent component, we can use react. Children if we need to work with the child component, so that we don’t get any problems with this.props. Children.

React.cloneElement

React.Children may appear with a react. cloneElement. Therefore, we also need to introduce React. CloneElement.

The React element is immutable, and children are not, in fact, children. It is just a children descriptor, and we cannot modify any of its properties, only read its contents, so react. cloneElement allows us to copy its elements and modify or add new props for our purposes [2].

For example, sometimes we need to do further processing on the child element, but since the React element itself is immutable, we need to clone it for further processing. In this Menu component, we want its children to be of either MenuItem or SubMenu type, and warning if they are of any other type. Specifically, the code can be written roughly like this:

if (displayName === 'MenuItem' || displayName === 'SubMenu') {
  // Clone the React element and return the new React element with the clone as the first argument
  return React.cloneElement(childElement, {
    index: index.toString()
  })
} else {
  console.error("Warning: Menu has a child which is not a MenuItem component")}Copy the code

How is parent component data passed to child components

Context is used to pass data from the parent to the child. If you are not familiar with Context, you can refer to the official documentation, Context. In the parent component we create the Context by createContext, and in the child component we get the Context by useContext.

Index data transfer

The main data transfer variable in the Menu component is index.

Finally, attach the complete code, first is the Menu parent component:

import React, { useState, createContext } from 'react'
import classNames from 'classnames'
import { MenuItemProps } from './menuItem'

type MenuMode = 'horizontal' | 'vertical'
type SelectCallback = (selectedIndex: string) = > void

exportinterface MenuProps { defaultIndex? : string;// For which menu subcomponent is highlightedclassName? : string; mode? : MenuMode; style? : React.CSSProperties; onSelect? : SelectCallback;// A callback can be triggered when a submenu is clickeddefaultOpenSubMenus? : string[]; }// Determine the data type to be passed from parent to child
interface IMenuContext {
  index: string; onSelect? : SelectCallback; mode? : MenuMode; defaultOpenSubMenus? : string[];// You need to pass data to context
}

// Create the context to pass to the child component
// Generic constraint, since index is the value to be entered, write a default initial value here
export const MenuContext = createContext<IMenuContext>({index: '0'})

const Menu: React.FC<MenuProps> = (props) = > {
  const { className, mode, style, children, defaultIndex, onSelect, defaultOpenSubMenus} = props
  // There should be only one MenuItem in the active state. Use useState to control its state
  const [ currentActive, setActive ] = useState(defaultIndex)
  const classes = classNames('menu-demo', className, {
    'menu-vertical': mode === 'vertical'.'menu-horizontal': mode === 'horizontal'
  })

  // Define handleClick to implement the active change after clicking menuItem
  const handleClick = (index: string) = > {
    setActive(index)
    // onSelect is a union type. It may or may not exist
    if (onSelect) {
      onSelect(index)
    }
  }

  // When a child component is clicked, the onSelect function is triggered to change the highlighting
  const passedContext: IMenuContext = {
    / / currentActive string | undefined type, the index is number type, so to do further defined types as follows
    index: currentActive ? currentActive : '0'.onSelect: handleClick, // The callback function that fires when a child component is clicked
    mode: mode,
    defaultOpenSubMenus,
  }

  const renderChildren = () = > {
    return React.Children.map(children, (child, index) = > {
      // Child contains a bunch of types. To get the type we want to provide intelligent hints, use type assertions
      const childElement = child as React.FunctionComponentElement<MenuItemProps>
      const { displayName } = childElement.type
      if (displayName === 'MenuItem' || displayName === 'SubMenu') {
        // Clone the React element and return the new React element with the clone as the first argument
        return React.cloneElement(childElement, {
          index: index.toString()
        })
      } else {
        console.error("Warning: Menu has a child which is not a MenuItem component")}}}return (
    <ul className={classes} style={style}>
      <MenuContext.Provider value={passedContext}>
        {renderChildren()}
      </MenuContext.Provider>
    </ul>
  )
}

Menu.defaultProps = {
  defaultIndex: '0'.mode: 'horizontal'.defaultOpenSubMenus: []}export default Menu
Copy the code

Then there are the MenuItem subcomponents:

import React from 'react'
import { useContext } from 'react'
import classNames from 'classnames'
import { MenuContext } from './menu'

export interface MenuItemProps {
  index: string; disabled? : boolean; className? : string; style? : React.CSSProperties; }const MenuItem: React.FC<MenuItemProps> = (props) = > {
  const { index, disabled, className, style, children } = props
  const context = useContext(MenuContext)
  const classes = classNames('menu-item', className, {
    'is-disabled': disabled,
    // Implement highlighted concrete logic
    'is-active': context.index === index
  })
  const handleClick = () = > {
    // Disabled will not be able to use onSelect, index may not exist because it is optional, need to use typeof to determine
    if(context.onSelect && ! disabled && (typeof index === 'string')) {
      context.onSelect(index)
    }
  }
  return (
    <li className={classes} style={style} onClick={handleClick}>
      {children}
    </li>
  )
}

MenuItem.displayName = 'MenuItem'
export default MenuItem
Copy the code

Finally, the SubMenu subcomponent:

import React, { useContext, FunctionComponentElement, useState } from 'react'
import classNames from 'classnames'
import { MenuContext } from './menu'
import { MenuItemProps } from './menuItem'

exportinterface SubMenuProps { index? : string; title: string; className? : string }const SubMenu: React.FC<SubMenuProps> = ({ index, title, children, className }) = > {
  const context = useContext(MenuContext)
  // We are going to use some of the methods of the string array, so we do type assertion first and declare it to be the string array type
  const openedSubMenus = context.defaultOpenSubMenus as Array<string>
  // use include to check whether there is an index
  const isOpened = (index && context.mode === 'vertical')? openedSubMenus.includes(index) :false
  const [ menuOpen, setOpen ] = useState(isOpened)  // isOpened returns true or false, which is a dynamic value
  const classes = classNames('menu-item submenu-item', className, {
    'is-active': context.index === index
  })
  // Used to show or hide the drop-down menu
  const handleClick = (e: React.MouseEvent) = >{ e.preventDefault() setOpen(! menuOpen) }let timer: any
  // Toggle is used to determine whether to turn it on or off
  const handleMouse = (e: React.MouseEvent, toggle: boolean) = > {
    clearTimeout(timer)
    e.preventDefault()
    timer = setTimeout(() = > {
      setOpen(toggle)
    }, 300)}// A ternary expression, vertical
  const clickEvents = context.mode === 'vertical' ? {
    onClick: handleClick
  } : {}
  const hoverEvents = context.mode === 'horizontal' ? {
    onMouseEnter: (e: React.MouseEvent) = > { handleMouse(e, true)},onMouseLeave: (e: React.MouseEvent) = > { handleMouse(e, false},} : {}// Used to render the contents of the dropdown menu
  // Returns two values, the first child and the second index, represented by I
  const renderChildren = () = > {
    const subMenuClasses = classNames('menu-submenu', {
      'menu-opened': menuOpen
    })
    // Only menuItems can be found in subMenu
    const childrenComponent = React.Children.map(children, (child, i) = > {
      const childElement = child as FunctionComponentElement<MenuItemProps>
      if (childElement.type.displayName === 'MenuItem') {
        return React.cloneElement(childElement, {
          index: `${index}-${i}`})}else {
        console.error("Warning: SubMenu has a child which is not a MenuItem component")}})return (
      <ul className={subMenuClasses}>
        {childrenComponent}
      </ul>)}return (
    // Expand operator, add function inside, hover outside
    <li key={index} className={classes} {. hoverEvents} >
      <div className="submenu-title" {. clickEvents} >
        {title}
      </div>
      {renderChildren()}
    </li>
  )
}

SubMenu.displayName = 'SubMenu'
export default SubMenu
Copy the code
The resources
  1. The React. The use of the Children
  2. The use of the React. CloneElement