Some reflections on the process of creating custom components: figuring out where to start + logical sequence of steps to implement, debugging, identifying what can be improved and factors that need to be taken into account. A structural overview of each component, and key takeaways. (To be updated: tags, advanced:autocomplete)
I keep forgetting why exactly I need to use arrow functions when passing event handlers.. the following explains why you need to use them to pass functions with arguments inline (I always revert back to this link every time):

Definition of a controlled component -
...in the controlled component the form input element’s values and mutations are totally driven by event handlers and the value of the input element is always inferred from the state
whereas an uncontrolled component -
doesn't use any states on input elements or any event handler ... doesn't care about an input element’s real-time value changes.
In short, the state of controlled components are controlled by React, vs. the state of uncontrolled components which are controlled by the DOM. Connecting a managed state with input value makes that input value a controlled component. Refs are used in uncontrolled components to access the input values. Uncontrolled components may be easier to use for smaller applications, but controlled components offer more effective control and consistency which is advantageous for bigger applications. React recommends using controlled components for implementing forms.



When a controlled input value is undefined, it essentially becomes uncontrolled, which triggers this error:


focus/blur methodsModalBackdrop is a parent component to ModalView in the DOM tree, which is why when the ModalView is clicked, the event bubbles to ModalBackdrop and triggers its onClick handler as well. This is problematic since we only want to trigger the handler when the click is detected outside ModalView. To do this, we need to use the stopPropagation() method within ModalView's event handler to suppress event bubbling. position: fixedstyled.div.attrs use caseThere is one state variable isOpen, and one event handler that toggles this state variable. When you click the ModalBtn component, its event handler toggles the isOpen state (turns it on), which then triggers a re-rendering of the Modal component. Since the state isOpen changed to true, the button text changes, and the ModalBackdrop and ModalView components are displayed. Clicking the x button or anywhere outside the ModalView window toggles the isOpen state off, which triggers a re-rendering that hides the modal state view (ModalBackdrop & ModalView).
//Styled Component Elements
import { useState } from 'react';
import styled from 'styled-components';
export const ModalContainer = styled.div`
display: flex;
flex-direction: row;
color: black;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
`;
export const ModalBackdrop = styled.div`
display: flex;
align-items: center;
justify-content: center;
position: fixed;
margin: 0;
width: 100vw;
height: 100vh;
background: rgba(48, 48, 48, 0.75);
`;
export const ModalBtn = styled.button`
background-color: var(--coz-purple-600);
text-decoration: none;
border: none;
padding: 20px;
color: white;
border-radius: 30px;
cursor: grab;
`;
export const ModalView = styled.div.attrs((props) => ({
role: 'dialog',
}))`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 400px;
height: 100px;
border: 1px solid black;
background: white;
border-radius: 25px;
text-decoration: none;
color: black;
`;
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = () => {
setIsOpen(!isOpen);
};
//you don't need to put another onclick handler in a child component, because it already exists in a parent component!
return ( //clicking backdrop and x closes the window (parent->child propagation), and clicking the dialog box you need to stop event bubbling
<>
<ModalContainer>
<ModalBtn onClick={openModalHandler}>
{isOpen ? 'Opened!' : 'Open Modal'}
</ModalBtn>
{isOpen ? <ModalBackdrop onClick={openModalHandler}>
<ModalView onClick={e => e.stopPropagation()}><div onClick={openModalHandler}>×</div>Hello Codestates!</ModalView>
</ModalBackdrop> : null}
</ModalContainer>
</>
);
};

transition property inside CSS selectorsThe togglehandler toggles the isOn state, which triggers a re-rendering of the component and adds a new class toggle--checked to the div components, which changes its CSS properties (toggles it on and off.)
import { useState } from 'react';
import styled from 'styled-components';
const ToggleContainer = styled.div`
position: relative;
margin-top: 8rem;
left: 47%;
cursor: pointer;
> .toggle-container {
width: 60px;
height: 30px;
border-radius: 40px;
background: #8b8b8b;
background: linear-gradient(to left, #8b8b8b 50%, var(--coz-purple-600) 50%) right;
background-size: 200%;
transition: .5s ease-out;
${'' /* when toggle--checked is also on, turn this on */}
&.toggle--checked {
background-position: left;
transition: .4s ease-out;
}
}
> .toggle-circle {
position: absolute;
top: 4px;
left: 4px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
transition: left 0.5s;
&.toggle--checked{
left: 34px;
transition: left 0.5s ease-out;
}
}
`;
const Desc = styled.div`
display: flex;
margin-top: 20px;
justify-content: center;
color: black;
${'' /* background: pink; */}
${'' /* width: 30%; */}
`;
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
setisOn(!isOn);
};
//onclick, trigger togglehandler
//if toggle is on, add toggle--checked class
//change description text depending on toggle state
return (
<>
<ToggleContainer onClick={toggleHandler}>
<div className={`toggle-container ${isOn ? 'toggle--checked' : ''}`}/>
<div className={`toggle-circle ${isOn ? 'toggle--checked' : ''}`}/>
</ToggleContainer>
{isOn ? <Desc>Toggle Switch On</Desc> : <Desc>Toggle Switch Off</Desc>}
</>
);
};

The menuArr array contains all the data for the tabs as individual objects with two keys: name, and content. Inside the TabMenu component, all the tabs are loaded as list items using the map function, each with an onclick handler that changes the currentTab index when clicked. When the state change triggers a re-rendering, the Desc component loads the content of the currentTab, and the focused class is added to the individual tab item that corresponds to the currentTab index (which changes its color.)
The Tabs component is re-rendered every time a tab is clicked, even when the currently displayed tab is clicked again . This can be revised so that state changes only occur when the clicked tab is different from the currently loaded one, which may decrease some redundant flickering.
menuArr includes data for all tabs currentTab (index), updated on click (selectMenuHandler updates state)TabMenu (actual tab) & Desc (tab content)TabMenu creates the overall tab line, and the class submenu within it determines the overall style of the individual tabs.focusedDesc defines the style for the tab content, which is also dependent on the currentTab indexconst TabMenu = styled.ul`
background-color: #dcdcdc;
color: rgba(73, 73, 73, 0.5);
font-weight: bold;
display: flex;
flex-direction: row;
justify-items: center;
align-items: center;
list-style: none;
margin-bottom: 7rem;
.submenu {
padding: 10px 50px 10px 4px;
color: darkgray;
width: calc(100%/3);
}
.focused {
color: white;
background-color: var(--coz-purple-600);
}
& div.desc {
text-align: center;
}
`;
const Desc = styled.div`
text-align: center;
`;
export const Tab = () => {
// manage what tab is currently selected
const [currentTab, setCurrentTab] = useState(0);
const menuArr = [
{ name: 'Tab1', content: 'Tab menu ONE' },
{ name: 'Tab2', content: 'Tab menu TWO' },
{ name: 'Tab3', content: 'Tab menu THREE' },
];
//triggered on clicking a different tab, tab content changes depending on what the currenttab is
const selectMenuHandler = (index) => {
setCurrentTab(index);
};
return (
<>
<div>
<TabMenu>
{/* adds a class depending on whether it is the currentab or not*/}
{/* onclick, the currenttab is updated (passes idx)*/}
{menuArr.map((menu, idx)=> {
return <li key={idx}
className={currentTab === idx ? 'submenu focused' : 'submenu'}
onClick={()=>selectMenuHandler(idx)}
>{menu.name}
</li>;
})}
</TabMenu>
<Desc>
{/* show content of current tab*/}
<p>{menuArr[currentTab].content}</p>
</Desc>
</div>
</>
);
};

The Tags Component loadstags (initialized with initialTags) as unordered list items with the map function. Each tag contains a "tag" class, with a "tag-title" and "tag-close-icon" class element stylized via styled components. Clicking the close-icon span element triggers the removeTags handler which updates the state of tags (filtering out the clicked element), and this state change triggers a re-rendering to update the view to match the new tags state. Typing something into the input box updates the newTag state, and what is typed(newTag) is added to tags when the Enter key is clicked (but only if the tag does not already exist in tags, and is not an empty string. the newTag state is initialized into an empty string once tag adding is complete.) When the view is updated with the state change, the new tag appears alongside pre-existing ones.
TagsInput: styled component that defines the following styles: how the tags appear in a row, individual tags when added (including the tag close icon), and input box for creating new tags (including when the box is focused) tags (current tag list), and newTag (new tag being made before it is added) newTag is updated with every key being typed, and only when the Enter key is clicked is the newTag added to the tags list and then initialized to an empty string tags is initialized with two strings in initialTagsaddTags & removeTagsaddTags: triggered when Enter key is clicked from input boxnewTag stateremoveTags: triggered when x button is clicked. the tag's index is passed to the handler, which uses the index to filter the current tag list (the most compact way of updating current tag list) //Tag.js
import { useState } from 'react';
import styled from 'styled-components';
export const TagsInput = styled.div`
margin: 8rem auto;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
min-height: 48px;
width: 480px;
padding: 0 8px;
border: 1px solid rgb(214, 216, 218);
border-radius: 6px;
> ul {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 8px 0 0 0;
> .tag {
width: auto;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
padding: 0 8px;
font-size: 14px;
list-style: none;
border-radius: 6px;
margin: 0 8px 8px 0;
background: var(--coz-purple-600);
> .tag-title{
color: white;
}
> .tag-close-icon {
display: block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
font-size: 10px;
margin-left: 8px;
color: var(--coz-purple-600);
border-radius: 50%;
background: #fff;
cursor: pointer;
}
}
}
> input {
flex: 1;
border: none;
height: 46px;
font-size: 14px;
padding: 4px 0 0 0;
:focus {
outline: transparent;
}
}
&:focus-within {
border: 1px solid var(--coz-purple-600);
}
`;
export const Tag = () => {
const initialTags = ['CodeStates', 'kimcoding'];
const [tags, setTags] = useState(initialTags);
const [newTag, setNewTag] = useState('');
//triggered when x is clicked
//copies taglist to avoid directly changing state variable
const removeTags = (indexToRemove) => {
//const tagsCopy = tags.slice();
//tagsCopy.splice(indexToRemove, 1)
//setTags(tagsCopy);
//could use filter instead!
setTags(tags.filter((el, idx) => idx !== indexToRemove))
};
//in addition to adding tags when enter is clicked, this method
//1. checks if the tag already exists before adding the tag
//2. if there is no input, does not add tag
//3. once a tag is added, the input value is emptied
const addTags = (event) => {
if(event.target.value.length){ //when there is no input, no tag added
if(!tags.includes(event.target.value)){
setTags([...tags, event.target.value]);
setNewTag('');
}
}
};
const newTagHandler = e => {
setNewTag(e.target.value);
}
return (
<>
<TagsInput>
<ul id="tags">
{tags.map((tag, index) => (
<li key={index} className="tag">
<span className="tag-title">{tag}</span>
<span className="tag-close-icon" onClick={()=> removeTags(index)}>×</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
value={newTag}
onChange={newTagHandler}
onKeyUp={(e) => {
{
if (e.key==='Enter'){
addTags(e);
}
}
}}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};


useEffect to initialize dropdown selected state to -1 whenever the input changesuseEffect to update options as input value changesfilter instead of pushing related elements in new array (my initial choice) to filter relevant optionshasText : state for deciding whether to show dropdown or notinputValue : controlled component options: state for options relevant to changing input value
onBlur: event when an input loses focus (e.g. the user clicks somewhere outside the input), can be hooked to an event handler that exits edit mode when this happensWhen the span element is clicked, its event handler handleClick changes the isEditMode state to true, and this state change triggers the following:
MyInput component which then displays the InputEdit component instead of the span element MyInput component receives focus, since a useEffect hook is used to turn the Ref focus on with isEditMode as a dependencyIn edit mode we can type the text we want into the input, and any changes in input fires the handleInputChange handler, which updates the state variable newValue to match what is being typed. To exit edit mode, we can either press and either press the Enter key or click anywhere else with the mouse. Any key or blur event invokes the handleBlur handler which does the following:
isEditMode to false (if it's a key event, only when the key is 'Enter')newValue to handleValueChange to update the parent component's state variable name & age, which triggers a re-rendering of the parent component to reflect the changes in its text description MyInput child components are then re-rendered to update newValue to match the parent state variable value through useEffectMyInput component (each input component)value : for initializing newValue(input value state)handleValueChange : updates state variable managed in parent component to reflect input changes in child componentinputEl: reference for the input component (to turn its focus on when in edit mode), linked to input ref propertyisEditMode : switch condition for entering and exiting edit modenewValue : updated input value, linked to input value propertyhandleClick : when span is clicked, turns edit mode on handleBlur : when anywhere else is clicked, turns edit mode off, then updates parent component state to match newValuehandleInputChange : updates newValue to match changes in inputClickToEdit Component name : initialized to cache object's name value, then passed down to MyInput as props age : initialized to cache object's age value, then passed down to MyInput as props handleValueChange : declared inline, passed down to MyInput as props to update parent state variables depending on input interactionexport const MyInput = ({ value, handleValueChange }) => {
const inputEl = useRef(null);
const [isEditMode, setEditMode] = useState(false);
const [newValue, setNewValue] = useState(value);
useEffect(() => {
if (isEditMode) {
inputEl.current.focus();
}
}, [isEditMode]);
useEffect(() => {
setNewValue(value);
}, [value]);
const handleClick = () => { //change editState
// console.log('change mode!');
setEditMode(!isEditMode);
};
const handleBlur = (e) => { //change editState
if ( e.key && e.key==="Enter"){
setEditMode(false);
}else if (!e.key){ //if it's not a key event
setEditMode(false);
}
handleValueChange(newValue);
};
const handleInputChange = (e) => { //update value to what's typed
setNewValue(e.target.value);
};
return ( //onClick to the entire InputBox
<InputBox>
{isEditMode ? (
<InputEdit
type='text'
value={newValue}
ref={inputEl} //reference for each input element
onChange={handleInputChange} //when input changes, update state variable
onBlur={handleBlur} //when it loses focus, exit edit mode
onKeyDown={handleBlur} //on clicking enter, exit edit mode
/>
) : (
<span
onClick={handleClick} //when clicked, enter edit mode
>{newValue}</span>
)}
</InputBox>
);
}
const cache = {
name: '김코딩',
age: 20
};
export const ClickToEdit = () => {
const [name, setName] = useState(cache.name);
const [age, setAge] = useState(cache.age);
return (
<>
<InputView>
<label>이름</label>
<MyInput value={name} handleValueChange={(newValue) => setName(newValue)}/>
</InputView>
<InputView>
<label>나이</label>
<MyInput value={age} handleValueChange={(newValue) => setAge(newValue)}/>
</InputView>
<InputView>
<div className='view'>이름 {name} 나이 {age}</div>
</InputView>
</>
);
};
React: Responding to Events
Detect click outside React component
*What are controlled and uncontrolled components in React JS? Which one to use?
Controlled vs uncontrolled React components - why not both?
[TIL] styled-component 활용하기
Styled Components: Basics
12 Coding Examples of Ampersand Usages in Styled Components
Understanding styled components component selector and ampersand (StackOverflow)
Building accessible Select component in React
Codepen: Accessible Select: 4. Keyboard Interactions
Codepen: keyboard-navigable list in React