Refactoring a ReactJS class to a Hook issue - reactjs

I have the following class that works fine (I know that DOMSubtreeModified is depreacted, I will update this too). It's a very basic WYIWYG I'm trying to refactor to a hook:
export class TextEditorClass extends React.Component {
constructor(props) {
super(props)
this.state ={
// this props contains the html content of the contentEditable
content: this.props.content,
}
}
componentDidMount() {
// here I add a listener to the contentEditable div that calls updateContent
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
rangy.init();
}
setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
updateContent() {
this.props.setContent('content', document.getElementById('editor').innerHTML);
this.setState({
content: document.getElementById('editor').innerHTML,
})
}
render() {
return (
<div className='editor-content dashed'>
<input id="myInput" type="file" ref={(ref) => this.upload = ref} style={{ display: 'none' }} />
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
}
This is what I wrote so far:
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
function updateContent() {
value.dispatch({type: 'content', value: document.getElementById('editor').innerHTML});
}
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
But it doesn't work:
Warning: A component is changing a controlled input of type text to be
uncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
Second warning:
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
The reason behind this I suppose is the fact that the typing triggers a loop of rerenders. Why it doesn't happen in the class?
EDIT: I believe the key to fix the issue is in useEffect(); I suppose it behaves differently from componentDidMount, in fact in the hook version I had to swap these two lines to prevent another error from occurring from this:
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
to this:
document.getElementById("editor").innerHTML = this.props.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
EDIT 2:
Adding [] as a parameter for useEffect() I got rid of the first error:
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
}, []);
More info about it here: https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n

Your issue does not happen in React class component because componentDidMount is run once and you put the code of componentDidMount in useEffect without any conditions - which means it will run on each render.
Problems with your useEffect:
1) You are adding an event listender but you are not doing a clean-up afterwards.
2)You are mutating the DOM directly which is an anti pattern
3)You are applying any depedencies on your useEffect, which execut the useEffect on each render and will cause a memory leak.
Here is how your code should look like: (will be writing code shortly)
...
const [editorValue, setEditorValue] = React.useState('')
// If you want your hook to run once
React.useEffect( () => {
setEditorValue(value.state.content)
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
// The returning funnction will be executed on unmounting
() => document.getElementById("editor").removeEventListener("DOMSubtreeModified", () => updateContent(), false);
// note the `[]`] as second argument. That means to run only once.
}, [])
And the content of your #editor is then the controllable state editorValue.
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
{editorValue}
</div>

Matthew's answer pointed me to the right direction, and after a bit of tweaking I came up with this code that works fine:
import React from 'react'
import { Icon, IconButton, ButtonGroup, ButtonToolbar } from 'rsuite'
import rangy from 'rangy';
import 'rangy/lib/rangy-classapplier';
import dompurify from 'dompurify';
import './TextEditor.css';
import { ManagerContext } from './LessonManager.js';
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
// here I initialize the editor content with the content I receive from the provider
const [editorValue, setEditorValue] = React.useState(value.state.content);
React.useEffect( () => {
rangy.init();
const editor = document.getElementById('editor');
// here I replaced the deprecated DOMSubtreeModified with MutationObserver
// basically when you change the dom of the contentEditable I call dispatch
// and update the content
let mutationObserver = new MutationObserver(function(mutations) {
value.dispatch({type: 'content', value: editor.innerHTML});
});
mutationObserver.observe(editor, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
// the cleanup can be done with .disconnect();
return () => mutationObserver.disconnect();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
// the createMarkup() function is used to generate the markup that I will put inside
// my contentEditable, I installed an additional npm package (dompurify) to clean up
// the markup to prevent XSS attacks
// https://dev.to/jam3/how-to-prevent-xss-attacks-when-using-dangerouslysetinnerhtml-in-react-1464)
function createMarkup() {
const sanitizer = dompurify.sanitize;
return {__html: sanitizer(editorValue)}
};
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
// here I insert the html with the purified html
dangerouslySetInnerHTML={createMarkup()}
>
</div>
</div>
)
}
To give a complete answer I will add also the parent components. This is the LessonContents component:
import React, { useEffect } from 'react';
import { Input } from 'rsuite';
// TextEditorHook is the WYSIWYG component
import { TextEditorHook } from './TextEditor.js';
// this context contain the value of the lesson manager
// we will use the context for the lesson's title, desc, content and location
import { ManagerContext } from './LessonManager.js';
// truncate is a function to shorten text in order to make it fit
// maxLenght is the default max text length
import { truncate, maxLength } from '../Common.js';
export default function Menu() {
const value = React.useContext(ManagerContext);
useEffect(() => {
// if not editing inject the html in the display div on load
if (!value.editing) document.querySelector('.lesson-content').innerHTML = value.state.content;
console.log(value);
});
const editorContent = () => {
return (
<React.Fragment>
<Input
className='rsuite-input dashed'
placeholder='Lesson title'
value={value.state.title}
onChange={(v) => value.dispatch({type: 'title', value: v})}
/>
<Input
className='rsuite-input dashed'
componentClass="textarea"
rows={1}
style={{ width: '100%' }}
placeholder='Lesson description'
value={value.state.desc}
onChange={(v) => value.dispatch({type: 'desc', value: v})}
/>
<TextEditorHook/>
</React.Fragment>
)
}
const viewerContent = () => {
return (
<React.Fragment>
<div className='content'>
<span>{truncate(value.state.title, maxLength, false)}</span>
</div>
<div className='content'>
<span>{value.state.desc}</span>
</div>
<div className='content'>
<span>{truncate(value.state.location, maxLength, false)}</span>
</div>
<div className='content justify-left lesson-content'>
</div>
</React.Fragment>
)
}
return (
<React.Fragment>
{value.editing ? editorContent() : viewerContent()}
</React.Fragment>
)
}
And finally the LessonManager component:
import React from 'react';
import LessonMenu from './LessonMenu.js';
// this context will be used from child components to access the lesson status
import LessonContents from './LessonContents.js';
export const ManagerContext = React.createContext(null);
const initialState = {
title: 'Lesson title',
desc: 'Lesson description',
content: 'Lesson content',
location: 'home / english'
};
function reducer(state, action) {
console.log(action.type, action.value, state)
switch (action.type) {
case 'title':
return {...state, title: action.value};
case 'desc':
return {...state, desc: action.value};
case 'content':
return {...state, content: action.value};
default:
throw new Error();
}
}
export default function LessonManager() {
const [editing, toggleEditor] = React.useState(true);
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = React.useMemo(() => {
return {
state,
dispatch,
editing,
toggleEditor,
}
}, [state, editing]);
return (
<ManagerContext.Provider value={value}>
<div className='box-default expand'>
<div className='handle' style={{display: 'flex', justifyContent: 'center', width: '100%', cursor: 'grab'}}>
<LessonMenu />
</div>
<LessonContents />
</div>
</ManagerContext.Provider>
)
}
I am not completely sure this is the best approach to solve this problem, but it works fine.

Related

Why am I receiving this error "Uncaught TypeError: state.map is not a function" it only happens when I set my setState in my handleClick function

It's saying state.map is not a function but it works just fine before i call the handleclick function.I am stuck here and would like to know why its giving me the state.map is not a function error. Added bonus too would
be me asking how does one setState when you have objects inside objects.
For example if id like to return with setStatea {...item, value:false} value is inside and object thats inside and object. How would I specifically target the value I need inside and array of objects. Thank you.
import React from "react";
import Quiz from "./components/Quiz";
import Menu from "./components/Menu";
import { v4 as uuidv4 } from 'uuid'
function App() {
const [state,setState] = React.useState([])
const [start,setStart] = React.useState(false)
React.useEffect(()=> {
if(start === true){
console.log(state)
fetch("https://opentdb.com/api.php?amount=5&category=27&difficulty=easy&type=multiple")
.then(res => res.json())
.then(data => setState(data.results.map(item =>
({selectedQuestion: "",
buttons: item.incorrect_answers.concat(item.correct_answer).map(item =>
({use: item ,value: false, id: uuidv4()})),
questions: item.question
}))))
}} , [start])
function startGame () {
setStart(prevState => !prevState)
}
const myButtons = state.map(function(item) {
return (<Quiz
key={uuidv4()}
buttons={item.buttons}
value ={item.buttons.map(item => item.value)}
question = {item.questions}
handleClick ={(event) =>handleClick(event.target.value)}
/> ) })
console.log(state.map(item => item))
function handleClick (event) {
if (event === "false"){
//this is what is giving me issues//
setState(item => (
{
...item, selectedQuestion: "howdy"
}))
//////////////////////////////////////////////////////////////
}
else {
console.log("hi")
}
}
return (<div>
{start === false ? <Menu
startGame ={startGame}
/> : <div className="parent--quiz">
<div className="quiz">
{myButtons}
</div>
</div>
}
</div>
)
}
export default App;
Quiz component is here
function Quiz (props) {
const styles ={
backgroundColor: props.value === true ? "red" : "#D6DBF5"
}
return (
<div>
<div className="cards" style={{ borderTop: "2px solid #fff ", marginLeft: 20, marginRight: 20 }}>
<h1 className="quiz--h1">{props.question}</h1>
<div className="quiz--buttons-div" >
{props.buttons.map((item) => (
<button className="quiz--buttons" style={styles} value={item.value} onClick={props.handleClick}>{item.use}</button>
))}
</div>
</div>
</div>
)
}
export default Quiz
code sections are fine

TypeError: Cannot read property '0' of undefined when building my react app

while building my react app for deployment, I am getting this error
TypeError: Cannot read property '0' of undefined
when I am rending on port3000 I did not see this error but only get it while building the app.
Can anyone assist to resolve this?
import { useState } from "react";
import styles from "./Tabs.module.css"
const Tabs = ({ children}) => {
const [activeTab, setActiveTab] = useState (children [0].props.label);
const handleClick =( e, newActiveTab ) => {
e.preventDefault();
setActiveTab(newActiveTab);
}
return (
<div>
<ul className= {styles.tabs}>
{children.map ((tab) => {
const label = tab.props.label;
return (
<li
className= {label == activeTab ? styles.current : ""}
key= {label}
>
<a href="#" onClick={(e) => handleClick (e, label)}>{label}
</a>
</li>
)
})}
</ul>
{children.map ((tabcontent1) => {
if (tabcontent1.props.label == activeTab)
return (
<div key= {tabcontent1.props.label} className= {styles.content}>{tabcontent1.props.children}
</div>
);
})}
</div>
);
}
export default Tabs ;
In next js, when you don't put export const getServerSideProps = () => {} in your page then that page is automatically subjected to static side rendering. On development mode, you may see a lightening symbol on bottom-right. Anyway you can read the docs on data-fetching on nextjs. However, your issue on this situation can be easily fixed by setting the children through useEffect.
// handle null on your active tab render function
const [activeTab, setActiveTab] = useState(null);
useEffect(() => {
if(children.length)
children[0].props.label
}, [children])
Another Code Sample:
*A simple change in code structure and the way you are trying to do. It's on react but kind of same in next as well *
import React from "react";
const Tabs = ({ tabsData }) => {
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
const switchTabs = (index) => setActiveTabIndex(index);
return (
<div style={{ display: "flex", gap: 20, cursor: "pointer" }}>
{/* Here active tab is given a green color and non actives grey */}
{tabsData.map((x, i) => (
<div
key={i}
style={{ color: activeTabIndex === i ? "green" : "#bbb" }}
onClick={() => switchTabs(i)}
>
{x.label}
</div>
))}
{/* Show Active Tab Content */}
{tabsData[activeTabIndex].content}
</div>
);
};
export default function App() {
// You can place it inside tabs also in this case
// but lets say you have some states on this component
const tabsData = React.useMemo(() => {
return [
// content can be any component or React Element
{ label: "Profile", content: <p>Verify all Input</p> },
{ label: "Settings", content: <p>Settings Input</p> },
{ label: "Info", content: <p>INput info</p> }
];
}, []);
return (
<div className="App">
<Tabs tabsData={tabsData} />
</div>
);
}
and here is also a example sandbox https://codesandbox.io/s/serverless-night-ufqr5?file=/src/App.js:0-1219

Avoid re-creating body component on each render of react-modal

I'm using this lib to create a modal
I have 3 components: Table, Modal and List
Table has Modal (a custom React Modal), and the body of Modal will be List.
Now the problem is, List has some functions which change the states of Table, so when I do something that can make Table's state change, Table and Modal will be re-rendered when Modal is re-rendered, it re-creates a new List which leads to lost all stuffs I'm doing with List.
Here is a simple version of my app. link
Now I don't want List to be re-created each time Modal is re-rendered. Is there any way to archive that? (I don't want to create a modal myself or use global state management in this case)
import { useEffect, useMemo, useState } from "react";
import ReactModal from "react-modal";
ReactModal.setAppElement("#root");
const List = ({ onClick }) => {
useEffect(() => {
console.log("List is mounted");
}, []);
return <button onClick={onClick}>Click me!</button>;
};
const Modal = ({ state, body, isOpen }) => {
useEffect(() => {
console.log("Modal is re-rendered");
});
return (
<div
id="react modal wrapper"
style={{
display: `${isOpen ? "block" : "none"}`
}}
>
<ReactModal isOpen={isOpen}>
<div>
state is {state}
<br />
{body}
</div>
</ReactModal>
</div>
);
};
const Table = ({ state, onClick, isOpen }) => {
useEffect(() => {
console.log("Table is re-rendered");
});
const memorizedList = useMemo(() => <List onClick={onClick} />, []);
return (
<div>
state: {state}
<Modal state={state} body={memorizedList} isOpen={isOpen} />
</div>
);
};
const App = () => {
const [state, setState] = useState(1);
const onClick = () => setState((v) => v + 1);
return (
<div>
<button onClick={onClick}>Change state</button>
<Table state={state} onClick={onClick} isOpen={state % 2 === 0} />
</div>
);
};
export default App;

Passing Property To Another Component - React

I need to pass "notecards" (an array) down from "Notecard.js" to "LoadQuestions.js". Console log shows that it is passing, but when I use {notecards} within the "return" it errors as "undefined". Could you please take a look?
Notecard.js (without the imports):
const useStyles = makeStyles((theme) => ({
root: {
maxWidth: 345,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
}));
export default function Notecard( {notecards} ) {
const classes = useStyles();
const next = () => {
console.log('Next Button Clicked')
};
const previous = () => {
console.log('Back Button Clicked')
};
const hint = () => {
console.log('Hint Button Clicked')
};
console.log({notecards});
return (
<Card className={classes.root}>
<div id="cardBody">
<CardHeader
title="Kate Trivia"
// subheader="Hint: In the 20th century"
/>
<CardContent>
<LoadQuestions notecards={notecards}/>
</CardContent>
</div>
</Card>
);
}
LoadQuestions.js (without imports)
const {useState} = React;
export default function LoadQuestions( {notecards} ) {
const [currentIndex, setCounter] = useState(0);
console.log({notecards});
return (
<div>
<Toggle
props={notecards}
render={({ on, toggle }) => (
<div onClick={toggle}>
{on ?
<h1>{props.notecards} hi</h1> :
<h1>{this.props[currentIndex].backSide}</h1>
}
</div>
)}
/>
<button onClick={() => {
console.log({notecards})
if (currentIndex < (this.props.length-1)) {
setCounter(currentIndex + 1);
} else {
alert('no more cards')
}
}}>Next Card
</button>
<button onClick={() => {
if (currentIndex > 0 ) {
setCounter(currentIndex -1);
} else {
alert('no previous cards')
}
}}>Previous Card
</button>
</div>
);
}
Thanks in advance!
That's all the details I have for you, but stack overflow really wants me to add more before it will submit. Sorry!
You should check if props exists, first time it renders the component it has no props so it shows undefined.
First i must say you destructured notecards out, so no need to use props.
If you want to use props you should change
({notecards}) to (props)
and if not you can directly use notecards since it is destructured
I suggest you two ways
adding question mark to check if exists
<h1>{props?.notecards} hi</h1>//in the case you want to use props
or
add the props in a if statement
<h1>{props.notecards?props.notecards:''} hi</h1> // if notecards is destructured remove the "props."

How to use my react component PlaceInput to achieve place autocomplete in the menu input box?

I have a PlaceInput component which support google place autocomplete.
class PlaceInput extends Component {
state = {
scriptLoaded: false
};
handleScriptLoaded = () => this.setState({ scriptLoaded: true });
render() {
const {
input,
width,
onSelect,
placeholder,
options,
meta: { touched, error }
} = this.props;
return (
<Form.Field error={touched && !!error} width={width}>
<Script
url="https://maps.googleapis.com/maps/api/js?key={my google api key}&libraries=places"
onLoad={this.handleScriptLoaded}
/>
{this.state.scriptLoaded &&
<PlacesAutocomplete
inputProps={{...input, placeholder}}
options={options}
onSelect={onSelect}
styles={styles}
/>}
{touched && error && <Label basic color='red'>{error}</Label>}
</Form.Field>
);
}
}
export default PlaceInput;
I also have a menu item which is an<Input> from semantic-ui-react. The frontend is like below:
The menu code is like below:
<Menu.Item>
<Input
icon='marker'
iconPosition='left'
action={{
icon: "search",
onClick: () => this.handleClick()}}
placeholder='My City'
/>
</Menu.Item>
How can I leverage the PlaceInput component to make menu <Input> box to achieve the place autocomplete? Thanks!
If you could share a working sample of your app (in e.g. codesandbox) I should be able to help you make your PlaceInput class work with the Menu.Input from semantic-ui-react.
Otherwise, you can test a fully working example of such integration with the code below, which is based off of the Getting Started code from react-places-autocomplete.
import React from "react";
import ReactDOM from "react-dom";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng
} from "react-places-autocomplete";
import { Input, Menu } from "semantic-ui-react";
const apiScript = document.createElement("script");
apiScript.src =
"https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places";
document.head.appendChild(apiScript);
const styleLink = document.createElement("link");
styleLink.rel = "stylesheet";
styleLink.href =
"https://cdn.jsdelivr.net/npm/semantic-ui/dist/semantic.min.css";
document.head.appendChild(styleLink);
class LocationSearchInput extends React.Component {
constructor(props) {
super(props);
this.state = { address: "" };
}
handleChange = address => {
this.setState({ address });
};
handleSelect = address => {
geocodeByAddress(address)
.then(results => getLatLng(results[0]))
.then(latLng => console.log("Success", latLng))
.catch(error => console.error("Error", error));
};
render() {
return (
<PlacesAutocomplete
value={this.state.address}
onChange={this.handleChange}
onSelect={this.handleSelect}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<Menu>
<Menu.Item>
<Input
icon="marker"
iconPosition="left"
placeholder="My City"
{...getInputProps({
placeholder: "Search Places ...",
className: "location-search-input"
})}
/>
</Menu.Item>
</Menu>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map(suggestion => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "#fafafa", cursor: "pointer" }
: { backgroundColor: "#ffffff", cursor: "pointer" };
return (
<div
{...getSuggestionItemProps(suggestion, {
className,
style
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
);
}
}
ReactDOM.render(<LocationSearchInput />, document.getElementById("root"));
Hope this helps!

Resources