Converting Material-UI to a class in react - reactjs

I am trying to use the 'Stepper' react material-ui component, but I am having difficulty using it in a class fashion, rather than function as they have in their previews.
Here is what I have so far, and it does load but with some problems:
The text that appears is 'unknown step' meaning that the function 'getStepContent' does not gets called properly
Every time I am hitting the 'next' button, it gives me an error saying: 'Cannot read property 'has' of undefined' seems like almost all of my function calls are messed up..
Here is my code:
import React, { Component } from "react";
import "./CharacterCreate.css";
import PropTypes from 'prop-types';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import Typography from '#material-ui/core/Typography';
import Box from '#material-ui/core/Box';
import { makeStyles } from '#material-ui/core/styles';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step';
import StepLabel from '#material-ui/core/StepLabel';
import Button from '#material-ui/core/Button';
export default class CharacterCreate extends Component {
constructor(props) {
super(props);
this.state = {
activeStep: 0,
skipped :new Set()
};
this.handleNext = this.handleNext.bind(this);
this.isStepSkipped = this.isStepSkipped.bind(this);
}
getSteps() {
return ['Select campaign settings', 'Create an ad group', 'Create an ad'];
}
getStepContent(step) {
switch (step) {
case 0:
return 'Select campaign settings...';
case 1:
return 'What is an ad group anyways?';
case 2:
return 'This is the bit I really care about!';
default:
return 'Unknown step';
}
}
isStepOptional(step) {
return step === 1;
}
isStepSkipped(step) {
return this.state.skipped.has(step);
}
handleNext() {
let newSkipped = this.skipped;
if (this.isStepSkipped(this.activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(this.activeStep);
}
this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setState({skipped: this.skipped});
}
handleBack() {
this.setState({activeStep: prevActiveStep => prevActiveStep - 1})
}
handleSkip() {
if (!this.isStepOptional(this.activeStep)) {
// You probably want to guard against something like this,
// it should never occur unless someone's actively trying to break something.
throw new Error("You can't skip a step that isn't optional.");
}
this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setSkipped(prevSkipped => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(this.activeStep);
return newSkipped;
});
}
handleReset() {
this.setState({activeStep: 0})
}
render() {
const steps = this.getSteps();
return (
<div className="root">
<Stepper activeStep={this.activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (this.isStepOptional(index)) {
labelProps.optional = <Typography variant="caption">Optional</Typography>;
}
if (this.isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
<div>
{this.activeStep === steps.length ? (
<div>
<Typography className="instructions">
All steps completed - you&apos;re finished
</Typography>
<Button onClick={this.handleReset} className="button">
Reset
</Button>
</div>
) : (
<div>
<Typography className="instructions">{this.getStepContent(this.activeStep)}</Typography>
<div>
<Button disabled={this.activeStep === 0} onClick={this.handleBack} className="button">
Back
</Button>
{this.isStepOptional(this.activeStep) && (
<Button
variant="contained"
color="primary"
onClick={this.handleSkip}
className="button"
>
Skip
</Button>
)}
<Button
variant="contained"
color="primary"
onClick={this.handleNext}
className="button"
>
{this.activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}
}
I know it's a lot, but I'm simply trying to use the same example code from material-ui website as a class instead of a function..
Thank you for your help!

I think you're wiping out this.state.skipped here, since this.skipped doesn't appear to be declared anywhere.
this.setState({skipped: this.skipped});
After this call, this.state.skipped is undefined, so calling this.state.skipped.has(...) blows up.
I suspect you meant to use this.state.skipped.
Another source of trouble might be a scope issue that arises from the way your click handlers are declared and attached, e.g. onClick={this.handleNext}.
TLDR: Try onClick={() => this.handleNext()} instead.
In javascript the scope (what this refers to) inside a method call is generally set to the object on which it was called.
So if you call this.handleNext(), references to this inside handleNext will be your component, as you expect.
However, if instead you do:
const {handleNext} = this;
handleNext();
The this reference may not be what you expect, because the method wasn't invoked as a method on your component. It was invoked as a standalone function, detached from your component. And this is effectively what happens when you pass an event handler down to another component. Inside the child component (the button, for example), your handler is just a function passed in as a prop, detached from your component:
// inside the button component
const {onClick} = this.props;
onClick(); // no scope; detached from your component
There are several ways to fix this, but the two most straightforward are:
Declare a new function that invokes the handler on the component:
onClick={ () => this.handleNext() }
Make your handler an arrow function, because arrow functions automatically adopt the parent scope where they're declared. So instead of this:
handleNext() {
let newSkipped = this.skipped;
...
Do this:
handleNext = () => {
let newSkipped = this.skipped;
Hope this helps. Sorry it's so long. Give it a shot and let me know.
Side note: you can do both of these in a single call:
this.setState({activeStep: prevActiveStep => prevActiveStep + 1})
this.setState({skipped: this.skipped});
this.setState({
activeStep: prevActiveStep => prevActiveStep + 1,
skipped: this.state.skipped
})

Related

Like Button with Local Storage in ReactJS

I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff

Stepper functionality using React

I'm trying to replicate this stepper like functionality using react.
https://www.commbank.com.au/retail/complaints-compliments-form?ei=CTA-MakeComplaint
Below is my stackblitz, How can I achieve this functionality without using any 3rd Party plugins.
https://stackblitz.com/edit/react-ts-9cfjs3
I set up the basics of the UI on codesandbox.
The main part of how this works is via scrollIntoView using a reference to the div element on each Step. It's important to note that this will work on every modern browser but safari for the smooth scrolling.
Obviously for the actual form parts and moving data around, all of that will still need to be implemented, but this demonstrates nearly all of the navigation/scrolling behaviors as your example.
For reference, here's the main code:
import { useEffect, useRef, useState } from "react";
import A from "./A";
import B from "./B";
import C from "./C";
import D from "./D";
import "./styles.css";
const steps = [A, B, C, D];
export default function App() {
const [step, setStep] = useState(0);
/** Set up a ref that refers to an array, this will be used to hold
* a reference to each step
*/
const refs = useRef<(HTMLDivElement | null)[]>([]);
/** Whenever the step changes, scroll it into view! useEffect needed to wait
* until the new component is rendered so that the ref will properly exist
*/
useEffect(() => {
refs.current[step]?.scrollIntoView({ behavior: "smooth" });
}, [step]);
return (
<div className="App">
{steps
.filter((_, index) => index <= step)
.map((Step, index) => (
<Step
key={index}
/** using `domRef` here to avoid having to set up forwardRef.
* Same behavior regardless, but with less hassle as it's an
* ordianry prop.
*/
domRef={(ref) => (refs.current[index] = ref)}
/** both prev/next handlers for scrolling into view */
toPrev={() => {
refs.current[index - 1]?.scrollIntoView({ behavior: "smooth" });
}}
toNext={() => {
if (step === index + 1) {
refs.current[index + 1]?.scrollIntoView({ behavior: "smooth" });
}
/** This mimics behavior in the reference. Clicking next sets the next step
*/
setStep(index + 1);
}}
/** an override to enable reseting the steps as needed in other ways.
* I.e. changing the initial radio resets to the 0th step
*/
setStep={setStep}
step={index}
/>
))}
</div>
);
}
And component A
import React, { useEffect, useState } from "react";
import { Step } from "./utils";
interface AProps extends Step {}
function A(props: AProps) {
const [value, setValue] = useState("");
const values = [
{ label: "Complaint", value: "complaint" },
{ label: "Compliment", value: "compliment" }
];
const { step, setStep } = props;
useEffect(() => {
setStep(step);
}, [setStep, step, value]);
return (
<div className="step" ref={props.domRef}>
<h1>Component A</h1>
<div>
{values.map((option) => (
<label key={option.value}>
{option.label}
<input
onChange={(ev) => setValue(ev.target.value)}
type="radio"
name="type"
value={option.value}
/>
</label>
))}
</div>
<button
className="next"
onClick={() => {
if (value) {
props.toNext();
}
}}
>
NEXT
</button>
</div>
);
}
export default A;

How can I test a click event which changes a useState state with enzyme?

I have the following component:
import React, { useState } from "react";
import { Button, ThirteenBold } from "#selfdecode/sd-component-library";
import { PlayIcon } from "assets/icons";
import { TourButtonProps } from "./interfaces";
import { WelcomeVideoModal } from "../../modals/welcome-video-modal";
/**
* The tour button.
*/
export const TourButton: React.FC<TourButtonProps> = (props) => {
const [isIntroVideoShowing, setIsIntroVideoShowing] = useState(false);
return (
<>
<WelcomeVideoModal
isOpen={isIntroVideoShowing}
onClickX={() => setIsIntroVideoShowing(false)}
data-test="tour-button-welcome-video"
/>
<Button
{...props}
width={["max-content"]}
variant="tour"
onClick={() => setIsIntroVideoShowing(true)}
data-test="tour-button"
>
<ThirteenBold
mr={["12px"]}
color="cl_blue"
width={["max-content"]}
letterSpacing={["1px"]}
display={["none", "block"]}
textTransform="uppercase"
>
welcome tour
</ThirteenBold>
<PlayIcon style={{ height: "30px", fill: "#4568F9" }} />
</Button>
</>
);
};
And the test coverage report is complaining that I am not testing both of the onClick events, which change the state.
I've tried two approaches, and both fail.
Approach one was to mock the useState and see if it gets called as I'd have expected it.
This was the test I tried:
const setState = jest.fn();
const useStateMock: any = (initState: any) => [initState, setState];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
expect(button).toHaveLength(1);
button.simulate("click");
expect(setState).toHaveBeenCalled();
This shouldn't even be the final test, as it doesn't check what was the valuee it was called with, but still, it failed because useState wasn't even called.
The second approach I've tried was to check the prop value on this component:
<WelcomeVideoModal
isOpen={isIntroVideoShowing}
onClickX={() => setIsIntroVideoShowing(false)}
data-test="tour-button-welcome-video"
/>
And this is the test I've tried
test("Check the isIntroVideoShowing changes to true on buton click", () => {
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
const welcomeVideo = wrapper.find(
`[data-test="tour-button-welcome-video"]`
);
expect(button).toHaveLength(1);
expect(welcomeVideo.prop("isOpen")).toEqual(false);
button.simulate("click");
expect(welcomeVideo.prop("isOpen")).toEqual(true);
});
This one failed claiming it was called with false even after the click.
Is there a way to make these work? Or a different approach to cover these?
You need to give wrapper.update for updating the template with state changes after simulating the click event.
test("Check the isIntroVideoShowing changes to true on buton click", () => {
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
const welcomeVideo = wrapper.find(
`[data-test="tour-button-welcome-video"]`
);
expect(button).toHaveLength(1);
expect(welcomeVideo.prop("isOpen")).toEqual(false);
button.simulate("click");
wrapper.update();
expect(welcomeVideo.prop("isOpen")).toEqual(true);
});
Reference - https://enzymejs.github.io/enzyme/docs/api/ShallowWrapper/update.html

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

Why callback function in react-dnd useDrop hook can't update parent component state?

dnd Hook Api, and want to call parent component callback function to update state in useDrop drop function,
however I can only update the parent state from init state.
Here is an example, no matter how I drop the div to the dropTarget, the count is always update to 1, but when click button, it can update as I expected, why is this happen? and how to fix that?
https://codesandbox.io/s/reactdndexample3-gpc61?fontsize=14
import React, { useState } from 'react';
import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd, DropTarget } from 'react-dnd'
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend'
const { useDrag,useDrop } = dnd
function HookTest(props) {
const [count,setCount] = useState(0);
return (
<div>
<button onClick={()=>{setCount(count + 1)}}>clickToAdd</button>
<DropDiv addCount={()=>{setCount(count + 1)}}></DropDiv>
<DragDiv></DragDiv>
<div>{count}</div>
</div>
);
}
export default DragDropContext(HTML5Backend)(HookTest)
function DropDiv(props) {
const [collectedProps, drop] = useDrop({
accept:"widget",
hover:(item,monitor) => {
},
drop:(item,monitor)=>props.addCount(),
collect:(monitor) => {
return {
isOver:monitor.isOver({shallow: true}),
}
}
})
return (
<div ref={drop} style={{width:"100px",height:"100px",background:"blue"}}>
dropTarget
</div>
)
}
function DragDiv(props) {
const [dragSource,drag] = useDrag({
item: {type:"widget"},
collect: monitor => ({
opacity: monitor.isDragging() ? 0.4 : 1,
}),
})
return (
<div ref={drag} style={{width:"100px",height:"100px",background:"red"}}>
DragSource
</div>
)
}
The update does not work because in the context of the created functions for onClick und addCount, count has always the value 0.
I recommend you use the alternate syntax of setCount and pass a function which gets the previous state as an argument and returns the updated state.
<button onClick={()=>{setCount(prevCount => prevCount + 1)}}>clickToAdd</button>
<DropDiv addCount={()=>{setCount(prevCount => prevCount + 1)}}></DropDiv>
Working example

Resources