Preface
As a newcomer who has just come into contact with react component design, it is very difficult to design and develop a component independently. This article describes in detail the whole process of developing the component of the channel selection page on the first page of the Mi You Club. I hope that the design and development of this simple component will help people like me who have been in touch with react component development very soon.
Preparatory phase
Page Analysis
Before you formally start copying a page, look at the effect of the original page:

- Increase Delete by Listening List Data state Change
- My Channel List is Long by Drag
- Remove pop-up alerts when there is only one game left in my channel list
- The data changes, and the OK button is highlighted in the tab
I divide the component file directory as follows:
SelectChannel ├─ Body │├─ content ││├─ index.jsx ││└─ style.js │├─ index.jsx │└─ style.js ├─ Footer │├─ content ││├─ index.jsx ││└─ style.js │├─ index.jsx │└─ style.js ├─ Header │├─ index.jsx │└─ style.js ├─ index.jsx └─ style.js
Using tools
vite: Scaffold, initialize react item dnd-kit: drag sort function is achieved by him, Official Documentsstyled-components: css in js,Official Documents classnames: Dynamic class name, Official Documents fastmock: interface fake data axios: data request
Development phase
1. Initialize the project
- The terminal npm init @vitejs/app initializes the project, enters the project name as prompted, selects react, and opens the generated vite configuration file to set the src directory alias @
- fastmock prepares interface fake data and requests data in the api directory without making data requests in the component: data
- iconfont select the desired icon to be similar and unzip the assets directory
2. Mobile Adaptor
- Mobile page development certainly requires adapters * Create a JS file adapter in the public directory. JS are as follows: var init = function () {var clientWidth = document.documentElement.clientWidth | document.body.clientWidth; if (clientWidth >= 640) {clientWidth = 640;} Var fontSize = (20/375) * clientWidth; Document.documentElement.style. FontSize = fontSize +'px';}; Init(); Window. AddEventListener ('resize', init);* Create directory modules under src to create rem.js as follows: document.documentElement.style. FontSize = document.documentElement. ClientWidth / 3.75 +'px'; // Switch windows horizontally and vertically. Onresize = function () {document.documentElement.style.fontSize = document.documentElement.clientWidth / 3.75 +'px';} * Reference adapter in index.html. Js, main. Reference rem.js in JSX
3. Implement the parent component SelectChannel
- Except for the unique parts of the subcomponents, data state changes and functions occur in the parent component and are passed to the subcomponents with the following complete files:
export default function SelectChannel() { const [list, setList] = useState([ { id: 7, title: 'Big Field', img: 'https://Bbs.mihoyo.com/_ Nuxt/img/game-dby.7b16fa8.jpg', checked: true,},]; Const [load, setLoading] = useState (false) const [change, setChange] = useState (false)//Filter out the selected and unselected items const TrueCheck = list. Filter (item => item.checked == true); Const FalseCheck = list. Filter (item => item.checked == false); // Prompt the modal box const modal=()=>{return (loading && <Modal> <span>select at least one game oh~</span> </Modal>)} //Timer to let the modal box disappear const setState = () =>{setTimeout(()=>{setLoading(false)}, 2000)} //Choose const choose = item => {// console.log ('------'); let IDX = list.findIndex (data => item.id === data.id); //console.log (idx); list[idx].checked =! List[idx]. checked; setList ([... list]); setChange (true)}; //console. Delete the selected item const deleteList = item => {let IDX = list.findIndex (data => item.id == data.id); //Determine if the selected item is less than or equal to two, if it is, then cannot be deleted, pop up the prompt modal box, if it is greater than two, perform the deletion if (TrueCheck.length <= 2) {setLoading (true); setState ();} Else{list[idx]. checked =! List[idx]. checked; setList ([... list]); setChange (true)}}; // Take data useEffect () => {(async () => {let {data} = await select (); // Console. Log (data); SetList ([... list,... Data]);}) ();}, [];// Dradrag-and-drop sort const handleDragEnd = ({active, over}) =>{if (active.id!===== over.id) =>{if (active.id!======= over.id) {setList ((items) =>{{const oldIndex = items.findIndex (item => item.id ======active.id) const newIndex = items.findIndex = items.findIndex (item =>>item.id ========== active.id) return arrayMoayayitems, index, index, index, newIndex} SetChange (true)}} setChange (return < < {{{modal returnreturnreturnreturnreturnreturns ()} <Header change={change} /> <Content data={list} deleteList={deleteList} HandleDragEnd={handleDragEnd} /> <Footer data={list} select={select} FalseCheck={FalseCheck} /> </>;
3.1 Small Modal Box
- Give the small modal box component a status loading default is false When triggering the delete function, determine the length of array data in my channel, change the loading status
const [loading,setLoading] = useState(false)const deleteList = item => {let idx = list.findIndex(data => item.id === data.id);// Determine if the selected item is less than or equal to two, if it is, then it cannot be deleted, pop up the prompt mode box, and if it is greater than two, execute the delete if (TrueCheck.length <= 2) {setLoading (true); setState ();} Else{list[idx]. checked =! List[idx]. checked; setList ([... list]); setChange (true)}};
- When I click Delete when there are only two data lengths left in my channel, a pop-up prompt pops up. The original page knows that the entire page has this prompt data, so I can write to death.
const [loading,setLoading] = useState(false)// Prompt modal box const modal=()=>{return (loading && <Modal>//no other pop-up items, pop-up data write-dead <span>select at least one game oh~</span></Modal>)}//Timer to make modal box disappear const setState = () =>{setTimeout(()=>{setLoading(false)}, 2000)}
3.2 Delete and add functions
- Logically, findIndex finds the data in the list, compares it to the id of the item passed in from the sub-component triggering event, changes the checked found data, and setList changes the data displayed in the list by the two components
// Select const select = item => {// console.log ('------'); let IDX = list.findIndex (data => item.id === data.id); //console.log (idx); list[idx].checked =! List[idx]. checked; list ([... list]); setChange (true)}; // Delete the selected item const deleteList = item => {let IDX = list.findIndex (data => item.id == data.id); //Determine if the selected item is less than or equal to two, if it is, then cannot be deleted, pop up the prompt modal box, if it is greater than two, execute the delete if (TrueCheck.length <= 2) {setLoading (true); setState ();} Else{list[idx]. checked =! List[idx]. checked; setList ([... list]); setChange (true)}};
3.3 Drag Sort
- Logic and deletion add roughly the same, calling the arrayMove function in dnd-kit to process the exchanged data
// Drag and drop to sort const handleDragEnd = ({active, over}) => {if (active.id!= over.id) {setList ((items) => {const oldIndex = items.findIndex (item => item.id === active.id) const newIndex = items.findIndex (item => item.id == over.id) return arrayMove (oldIndex, newIndex)}} setChange (true)}
4. Header tab
- A common three-column layout with two left and right clicks to jump to the first page. Here you can set the route, use Link, but here you can see a separate page component development, first with a tag instead, and then if you need to replace it
- Using classnames makes it very easy to set dynamic class names and use the chang value passed from the parent component to change whether the Confirm button is highlighted or not
The code is as follows:
export default function Header({change}) {return (<Tab><div className="left"><a href="#"><i className="iconfont icon-fanhui"></i></a></div><div className="content">Home Channel Selection</div><div className="right"><a href="#"ClassName={classnames("noChange", {changeItem: change})}>OK</a></div></Tab>); }
5. Implementation of My Channels and Recommended Channel Components
5.1 Component Analysis
My channel and the recommended channel have two parts, a fixed header, showing my channel and the recommended channel title. Below the header is the list component dynamically generated by map. My channel also needs to be dragged and sorted, so there is a sub-component ContentList added here
5.2 Drag and Sort Component Library Selection
- This component is a difficult part of the whole component implementation, drag-and-drop sorting is difficult to achieve. I tried to achieve it by using native react, but the result was not satisfactory. I finally decided to use the ready-made scheme. There are several common drag-and-drop libraries to choose from: * react-dnd github is a very popular drag-and-drop library with complete functions, but it seems a little too heavy for this page, so I gave up * react-beautiful-dnd and react-dnd are similar. But my download package doesn't seem to support react18 and I can't install it, so I sent * dnd-kit to Wuhu. Looking at the official documents, it's very easy to use them. Just drag the root component in DndContext, SortableContext, Sensors listen for different dragging devices, and the collision algorithm available in the component library.
5.3 My Channel Component Implementation
5.3.1 Parent Component Implementation
- Capturing sensors using hook useSensor in @dnd-kit/core
- Wrap the drag-and-drop root component using the DndContext SortableContext component in @dnd-kit/core
- Use verticalListSortingStrategy in @dnd-kit/modifiers to dynamically modify the motion coordinates detected by the sensor to limit the drag direction to vertical
The parent component code is as follows:
export default function Content(props) {const { data, deleteList, handleDragEnd } = props// Capture touch sensor const touchSensor = useSensor(TouchSensor,{activationConstraint:{delay: 300,tolerance: 10,})//Capture mouse const mouseSensor = useSensor (MouseSensor, {activationConstraint:{delay: 300, tolerance: 0,}) const sensors = useSensors (touchSensor, mouseSensor)return (<BodyWrapper><TabWrapper><BodyWrapper><TabWrapper><header><div className=<div className='left'><p>My channel</p></div><div className='right'='><p><p>Long Drag sorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsorsor</p></Header></TabWrapper>/// / DndContext Context ContertableContext Context Context Package Drag root component<DndContext sensors={{{sensors}collisionDetection={Center}onDragEndcloscloseDragEnd {{{ifiers={[restrictToVerticalAxis, restrictToWindowEdges]}><SortableContextitems={data.map (item => item.id)}strategy={verticalListSorting Strategy}>{data.map((item) =><ContentList key={item.id} deleteList={deleteList} item={item}{...item}/>)}</SortableContext></DndContext></BodyWrapper>;
5.3.2 Subcomponent Implementation
- Matching parent element id parameter with hook useSortable in @dnd-kit/sortable
- Use CSS in @dnd-kit/utilities with some CSS properties to style when dragging a selection
The code is as follows:
export default function ContentList(props) {const { checked, id, title, img, deleteList, item } = props;const {setNodeRef,attributes,listeners,transition,transform,isDragging} = useSortable({id: id})// Style const style = {transition,transform: CSS.Transform.toString(transform), //drag-and-drop transparency, original version 1opacity: isDragging? 0.6: 1, dragSelectorExclude:'i'} return (<>{checked == true & & & &<Tabref={setNodeRef}{...attributes}{{... attributes}{{listeners}}{style}><TabItem><img sstem=={img}={img}}= {img} rcg}= //{imimg}}= {{{{img}} ALT /////>> {{{{{<span>{title}</span>{title!=='Big Field'& & & <i className= "Iconfont icon-shanjian" onClick={() => deleteList (item)} ></i>}<i className="iconfont icon-shouye" ></i></TabItem></Tab>}</>)
No style changes when dragging officially I give this 0.6 transparency
5.4 Recommended Channel Component Implementation
- Almost the same as my channel except no drag and drop sort
- Determine the length of the FalseCheck array to control whether the component is displayed or not, if there is no data in the component list, not the component
The code is as follows:
5.4.1 Parent Component
export default function Footer(props) {const { data, choose, FalseCheck } = propsreturn (<FooterWrapper>{FalseCheck.length > 0 &&<TabWrapper><header><div className='left'><p>Recommended Channels</p></div></header></TabWrapper>}<ContentList data={data} choose={choose} /></FooterWrapper>); }
5.4.2 Subcomponents
export default function ContentList(props) {const { data , choose } = propsreturn (<Tab>{data.map((item) => item.checked == false &&<TabItem key={item.id}><img src={item.img} alt="" /><span>{item.title}</span><i className="iconfont icon-tianjia" onClick={() => choose(item)}></i></TabItem>)}</Tab>) }
The end result:


Final directory structure:
select-channel ├─ index.html ├─ package-lock.json ├─ package.json ├─ public │└─ js │ └─ adapter.js ├─ src │├─ api ││└─ request.js │├─ App.css │├─ App.jsx │├─ assets ││├─ font ││└─ styles ││ └─ reset.css │├─ components ││└─ SelectChannel ││ ├─ Body ││ │├─ content ││ ││├─ index.jsx ││ ││└─ style.js ││ │├─ index.jsx ││ │└─ style.js ││ ├─ Footer ││ │├─ content ││ ││├─ index.jsx ││ ││└─ style.js ││ │├─ index.jsx ││ │└─ style.js ││ ├─ Header ││ │├─ index.jsx ││ │└─ style.js ││ ├─ index.jsx ││ └─ style.js │├─ index.css │├─ main.jsx │└─ modules │ └─ rem.js └─ vite.config.js
Last
This is the whole process of this component implementation, which will continue to improve in the future. Fanmi Youth Club Home Channel Setup Page github page to see the effect directly: Real-time demonstration