backgroundImage carousel: Why wont my react.js array of local images render? - javascript

I'm adding an image carousel to my website. The div that is linked to carousel.js is totally blank. It seems that the images are not rendering. My images are local and stored in an array in carousel.js. I've gone through similar threads but haven't been able to get the images to render. For example, I've made the image size smaller, tried importing the images at the top of carousel.js, moved the images folder to the public folder generated by node module, and about ten other things. This is my first react project so any help would be appreciated.
The full project can be viewed on cloud 9 here
Here's my carousel.js:
import React from 'react';
import ReactDOM from 'react-dom';
const imgUrls = [
"./images/croissant.jpg",
"./images/herbal-tea.jpg",
"./images/matcha-latte.jpg",
"./images/mochaLatte.jpg",
"./images/waffle.jpg"
];
class Carousel extends React.Component {
constructor (props) {
super(props);
this.state = {
currentImageIndex: 0
};
this.nextSlide = this.nextSlide.bind(this);
this.previousSlide = this.previousSlide.bind(this);
}
previousSlide () {
const lastIndex = imgUrls.length - 1;
const { currentImageIndex } = this.state;
const shouldResetIndex = currentImageIndex === 0;
const index = shouldResetIndex ? lastIndex : currentImageIndex - 1;
this.setState({
currentImageIndex: index
});
}
nextSlide () {
const lastIndex = imgUrls.length - 1;
const { currentImageIndex } = this.state;
const shouldResetIndex = currentImageIndex === lastIndex;
const index = shouldResetIndex ? 0 : currentImageIndex + 1;
this.setState({
currentImageIndex: index
});
}
render () {
return (
<div className="carousel">
<Arrow direction="left" clickFunction={ this.previousSlide } glyph="◀" />
<ImageSlide url={ imgUrls[this.state.currentImageIndex] } />
<Arrow direction="right" clickFunction={ this.nextSlide } glyph="▶" />
</div>
);
}
}
const Arrow = ({ direction, clickFunction, glyph }) => (
<div
className={ `slide-arrow ${direction}` }
onClick={ clickFunction }>
{ glyph }
</div>
);
const ImageSlide = ({ url }) => {
const styles = {
backgroundImage: `url(${url})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
};
return (
<div className="image-slide" style={styles}></div>
);
}
ReactDOM.render(
<Carousel />,
document.getElementById('container')
);

If you are using webpack you need to use require('./imagedirectory')
So it is like this...
const imgUrls = [ require("./images/croissant.jpg") , require("./images/herbal-tea.jpg") , require("./images/matcha-latte.jpg") , require("./images/mochaLatte.jpg") , require("./images/waffle.jpg") ];

What about this :
const ImageSlide = ({ url }) => {
const img = require(`${url}`);
const styles = {
backgroundImage: `url(${img})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
};
return (
<div className="image-slide" style={styles}></div>
);
}

Related

React and Javascript: how can I move const out of function App()?

How can I move one or more const out of function App?
In the simple test App below, I'm using localStorage to store a value which determines if a div is dispayed. The handleToggle dismisses the div and stores a value in localStorage. Clearing localstorage and reloading shows the div again.
In a simple test App on localhost, this works. But in my more complex production App, I'm getting the error Invalid hook call. Hooks can only be called inside of the body of a function component , which has a myriad of fixes, one of which points out the issue may be that a const needs to be a separate function.
And so I'm thinking the issue is that I need to convert the two const to a function that can be placed right under the import blocks and out of the function App() block.
As a start, in this simple App, how can I move the two const out of function App()?
import './App.css';
import * as React from 'react';
function App() {
const [isOpen, setOpen] = React.useState(
JSON.parse(localStorage.getItem('is-open')) || false
);
const handleToggle = () => {
localStorage.setItem('is-open', JSON.stringify(!isOpen));
setOpen(!isOpen);
};
return (
<div className="App">
<header className="App-header">
<div>{!isOpen && <div>Content <button onClick={handleToggle}>Toggle</button></div>}</div>
</header>
</div>
);
}
export default App;
Edit: This is the full production file with Reza Zare's fix that now throws the error 'import' and 'export' may only appear at the top level on line 65:
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import {
Compose,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../../ui/util/async-components';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline,
'DIRECTORY': Directory,
};
// Added const
const getInitialIsOpen = () => JSON.parse(localStorage.getItem('is-open')) || false;
const App = () => {
const [isOpen, setOpen] = React.useState(getInitialIsOpen());
const handleToggle = () => {
localStorage.setItem('is-open', JSON.stringify(!isOpen));
setOpen(!isOpen);
};
function getWeekNumber(d) {
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
var weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
return [d.getUTCFullYear(), weekNo];
}
var result = getWeekNumber(new Date());
// errors out here: 'import' and 'export' may only appear at the top level.
export default class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool,
children: PropTypes.node,
};
// Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS
mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
state = {
renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
}
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
if (this.mediaQuery) {
if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.addListener(this.handleLayoutChange);
}
this.setState({ renderComposePanel: !this.mediaQuery.matches });
}
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
}
componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
}
componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.removeListener(this.handleLayouteChange);
}
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
handleLayoutChange = (e) => {
this.setState({ renderComposePanel: !e.matches });
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = (node) => {
this.node = node;
}
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
}
renderError = (props) => {
return <BundleColumnError multiColumn errorType='network' {...props} />;
}
render () {
const { columns, children, singleColumn, isModalOpen } = this.props;
const { renderComposePanel } = this.state;
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' />
// output of getInitialIsOpen
<div class='banner'>
{!isOpen && <div>Content <button onClick={handleToggle}>Toggle</button></div>}
</div>
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
);
}
return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
{columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {};
return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
</BundleContainer>
);
})}
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
</div>
);
}
}

React - generating a unique random key causes infinite loop

I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}

React-spring not animating as expected

So I'm using react-visibility-sensor with react-spring to animate text sliding char by char from any side I want.
In my home page the animation is running smoothly, I use it twice one from the right side and another from the top side.
When I switch routes and go to another page the animation does not work.
I have my code divided in an "Title" component "Char" component and a custom hook "useAnimatedText".
Title component:
import React from "react";
import VisibilitySensor from "react-visibility-sensor";
import useAnimatedText from "../../hooks/useAnimatedText";
import Char from './Char'
const Title = ({title, side}) => {
// HERE I CALL A CUSTOM HOOK THAT WILL DIFINE IF THE ELEMENT IS VISIBLE OR NOT
// AND HANDLE THE ANIMATION WHEN NECESSARY
const [isVisible, onChange, objArray] = useAnimatedText(title)
let elements = objArray.map((item, i) => {
return(
<Char
key={i}
isVisible={isVisible}
item={item}
delay={400 + (i * 40)}
side={side}
/>
)
})
console.log(isVisible)
return(
<VisibilitySensor onChange={onChange} >
<span className="title-box">
<h1 className="my-heading divided-heading">
{elements}
</h1>
<hr className="title-ruller"></hr>
</span>
</VisibilitySensor>
)
}
export default Title
Char component:
import { useSpring, animated } from "react-spring"
const Char = (props) => {
const { isVisible, item, delay, isBouncy, side} = props
const [ref, addBounce] = useBounce()
let springConfig = {}
if (side === 'right') {
springConfig = {
to: {
opacity: isVisible ? 1 : 0,
translateX : isVisible ? '0px' : '1000px'
},
config: { mass:2, tension: 200, friction: 30},
delay: delay
}
}
else if (side === 'top') {
springConfig = {
to: {
opacity: isVisible ? 1 : 0,
translateY: isVisible ? '0px' : '-500px'
},
config:{ mass:2, tension: 250, friction: 35},
delay: delay
}
}
const spring = useSpring({...springConfig})
return(
<animated.span
style={ spring }
className={isVisible ? 'is-visible' : 'is-not-visible'}
>
{item.char === ' ' ? <span> </span> : item.char}
</animated.span>
)
}
export default Char
This is the custom Hook:
import { useState } from "react";
import { stringToArray } from '../helpers'
// HOOK THAT HANDLES THE TEXT ANIMATION BY SETTING A STATE OF VISIBILITY
function useAnimatedText(string) {
const [isVisible, setVisibility] = useState(false);
const onChange = visiblity => {
visiblity && setVisibility(visiblity);
};
let objArray = stringToArray(string)
return [isVisible, onChange, objArray]
}
export default useAnimatedText
I did a console.log(isVisible) and the value was true but it was rendering in the page the spring values as if it was false(not visible).
I really can´t understand where I'm going wrong here, the only problem I have is when I'm not at my main route, could it be because of react-router-dom?
If someone has any clue, let me know.

Does changing the props of a child always re-render the parent even with React.memo?

I'm trying to prevent a modal element to re-render when it's supposed to be invisible.
The course I'm following does this by converting the component to a class based component and using shouldComponentUpdate() but I wanted to check if using React.memo() did the same thing.
I tried, and it doesn't, but I'm not sure why.
The component that should not render is this:
import React , {useEffect} from 'react'
import Aux from '../../../hoc/Aux';
import Backdrop from '../Backdrop/Backdrop';
import classes from './Modal.module.css';
const Modal = (props) => {
useEffect(() => {
console.log('[Modal.js] useEffect')
});
return (
<Aux>
<Backdrop show={props.show} clicked={props.modalClosed} />
<div
className={classes.Modal}
style={{
transform: props.show ? 'translateY(0)' : 'translateY(-100vh)',
opacity: props.show ? '1': '0'
}}>
{props.children}
</div>
</Aux>)
};
export default React.memo(Modal);
Which is managed by
import React, {Component} from 'react'
import Aux from '../../hoc/Aux';
import Burger from '../../components/Burger/Burger'
import BuildControls from '../../components/Burger/BuildControls/BuildControls'
import Modal from '../../components/UI/Modal/Modal'
import OrderSummary from '../../components/Burger/OrderSummary/OrderSummary'
const INGREDIENT_PRICES = {
salad: 0.5,
cheese: 0.4,
meat: 1.3,
bacon: 0.7
}
class BurgerBuilder extends Component {
state = {
ingredients: {
salad: 0,
bacon: 0,
cheese: 0,
meat: 0
},
totalPrice: 4,
purchasable: false,
purchasing: false
}
purchaseHandler = () => {
this.setState({purchasing: true});
};
purchaseCancelHandler = () => {
this.setState({purchasing:false});
};
purchaseContinueHandler = () => {
alert('You Continue!')
};
updatePurchaseState = (ingredients) => {
let purchasable = false;
for (let ingredient in ingredients){
if (ingredients[ingredient]>0){
purchasable = true;
break;
}
}
this.setState({purchasable:purchasable})
}
addIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
const updatedCount = oldCount +1;
const updatedIngredients = {
...this.state.ingredients
};
updatedIngredients[type] = updatedCount;
const priceAddition = INGREDIENT_PRICES[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice + priceAddition;
this.setState({totalPrice: newPrice, ingredients: updatedIngredients});
this.updatePurchaseState(updatedIngredients);
};
removeIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
if (oldCount <= 0)
return;
const updatedCount =oldCount -1;
const updatedIngredients = {
...this.state.ingredients
};
updatedIngredients[type] = updatedCount;
const priceAddition =INGREDIENT_PRICES[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice - priceAddition;
this.setState({totalPrice: newPrice, ingredients: updatedIngredients});
this.updatePurchaseState(updatedIngredients);
};
render () {
const disabledInfo = {
...this.state.ingredients
};
for (let key in disabledInfo){
disabledInfo[key] = disabledInfo[key] <= 0;
}
return (
<Aux>
<Modal show={this.state.purchasing} modalClosed={this.purchaseCancelHandler}>
<OrderSummary
ingredients={this.state.ingredients}
purchaseCancelled={this.purchaseCancelHandler}
purchaseContinued={this.purchaseContinueHandler}
price={this.state.totalPrice} />
</Modal>
<Burger ingredients={this.state.ingredients}/>
<BuildControls
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo}
price={this.state.totalPrice}
purchasable={this.state.purchasable}
ordered={this.purchaseHandler}/>
</Aux>
);
}
}
export default BurgerBuilder;
With BuildControls I change the Ingredients state; but not the props I pass to modal, purchasing and purchaseHandler
Does changing the ingredients prop I pass to it's child always prompt a re-render even when Modal itself is under React.memo() ?
You are changing one of the props you pass to Modal - the children prop. It is passed implicitly by adding children to a react element. And since you are changing the child element it will cause a re-render.

javascript/react dynamic height textarea (stop at a max)

What I'm trying to achieve is a textarea that starts out as a single line but will grow up to 4 lines and at that point start to scroll if the user continues to type. I have a partial solution kinda working, it grows and then stops when it hits the max, but if you delete text it doesn't shrink like I want it to.
This is what I have so far.
export class foo extends React.Component {
constructor(props) {
super(props);
this.state = {
textareaHeight: 38
};
}
handleKeyUp(evt) {
// Max: 75px Min: 38px
let newHeight = Math.max(Math.min(evt.target.scrollHeight + 2, 75), 38);
if (newHeight !== this.state.textareaHeight) {
this.setState({
textareaHeight: newHeight
});
}
}
render() {
let textareaStyle = { height: this.state.textareaHeight };
return (
<div>
<textarea onKeyUp={this.handleKeyUp.bind(this)} style={textareaStyle}/>
</div>
);
}
}
Obviously the problem is scrollHeight doesn't shrink back down when height is set to something larger. Any suggestion for how I might be able to fix this so it will also shrink back down if text is deleted?
ANOTHER SIMPLE APPROACH (without an additional package)
export class foo extends React.Component {
handleKeyDown(e) {
e.target.style.height = 'inherit';
e.target.style.height = `${e.target.scrollHeight}px`;
// In case you have a limitation
// e.target.style.height = `${Math.min(e.target.scrollHeight, limit)}px`;
}
render() {
return <textarea onKeyDown={this.handleKeyDown} />;
}
}
The problem when you delete the text and textarea doesn't shrink back is because you forget to set this line
e.target.style.height = 'inherit';
Consider using onKeyDown because it works for all keys while others may not (w3schools)
In case you have padding or border of top or bottom. (reference)
handleKeyDown(e) {
// Reset field height
e.target.style.height = 'inherit';
// Get the computed styles for the element
const computed = window.getComputedStyle(e.target);
// Calculate the height
const height = parseInt(computed.getPropertyValue('border-top-width'), 10)
+ parseInt(computed.getPropertyValue('padding-top'), 10)
+ e.target.scrollHeight
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
e.target.style.height = `${height}px`;
}
I hope this may help.
you can use autosize for that
LIVE DEMO
import React, { Component } from 'react';
import autosize from 'autosize';
class App extends Component {
componentDidMount(){
this.textarea.focus();
autosize(this.textarea);
}
render(){
const style = {
maxHeight:'75px',
minHeight:'38px',
resize:'none',
padding:'9px',
boxSizing:'border-box',
fontSize:'15px'};
return (
<div>Textarea autosize <br/><br/>
<textarea
style={style}
ref={c=>this.textarea=c}
placeholder="type some text"
rows={1} defaultValue=""/>
</div>
);
}
}
or if you prefer react modules https://github.com/andreypopp/react-textarea-autosize
Just use useEffect hook which will pick up the height during the renderer:
import React, { useEffect, useRef, useState} from "react";
const defaultStyle = {
display: "block",
overflow: "hidden",
resize: "none",
width: "100%",
backgroundColor: "mediumSpringGreen"
};
const AutoHeightTextarea = ({ style = defaultStyle, ...etc }) => {
const textareaRef = useRef(null);
const [currentValue, setCurrentValue ] = useState("");// you can manage data with it
useEffect(() => {
textareaRef.current.style.height = "0px";
const scrollHeight = textareaRef.current.scrollHeight;
textareaRef.current.style.height = scrollHeight + "px";
}, [currentValue]);
return (
<textarea
ref={textareaRef}
style={style}
{...etc}
value={currentValue}
onChange={e=>{
setCurrentValue(e.target.value);
//to do something with value, maybe callback?
}}
/>
);
};
export default AutoHeightTextarea;
Really simple if you use hooks "useRef()".
css:
.text-area {
resize: none;
overflow: hidden;
min-height: 30px;
}
react componet:
export default () => {
const textRef = useRef<any>();
const onChangeHandler = function(e: SyntheticEvent) {
const target = e.target as HTMLTextAreaElement;
textRef.current.style.height = "30px";
textRef.current.style.height = `${target.scrollHeight}px`;
};
return (
<div>
<textarea
ref={textRef}
onChange={onChangeHandler}
className="text-area"
/>
</div>
);
};
you can even do it with react refs. as setting ref to element
<textarea ref={this.textAreaRef}></textarea> // after react 16.3
<textarea ref={textAreaRef=>this.textAreaRef = textAreaRef}></textarea> // before react 16.3
and update the height on componentDidMount or componentDidUpdate as your need. with,
if (this.textAreaRef) this.textAreaRef.style.height = this.textAreaRef.scrollHeight + "px";
actually you can get out of this with useState and useEffect
function CustomTextarea({minRows}) {
const [rows, setRows] = React.useState(minRows);
const [value, setValue] = React.useState("");
React.useEffect(() => {
const rowlen = value.split("\n");
if (rowlen.length > minRows) {
setRows(rowlen.length);
}
}, [value]);
return (
<textarea rows={rows} onChange={(text) => setValue(text.target.value)} />
);
}
Uses
<CustomTextarea minRows={10} />
I like using this.yourRef.current.offsetHeight. Since this is a textarea, it wont respond to height:min-content like a <div style={{height:"min-content"}}>{this.state.message}</div> would. Therefore I don't use
uponResize = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(
this.getHeightOfText.current &&
this.setState({
heightOfText: this.getHeightOfText.current.offsetHeight
}),
20
);
};
componentDidMount = () => {
window.addEventListener('resize', this.uponResize, /*true*/)
}
componentWillUnmount = () => {
window.removeEventListener('resize', this.uponResize)
}
but instead use
componentDidUpdate = () => {
if(this.state.lastMessage!==this.state.message){
this.setState({
lastMessage:this.state.message,
height:this.yourRef.current.offsetHeight
})
}
}
on a hidden div
<div
ref={this.yourRef}
style={{
height:this.state.height,
width:"100%",
opacity:0,
zIndex:-1,
whiteSpace: "pre-line"
})
>
{this.state.message}
</div>
Using hooks + typescript :
import { useEffect, useRef } from 'react';
import type { DetailedHTMLProps, TextareaHTMLAttributes } from 'react';
// inspired from : https://stackoverflow.com/a/5346855/14223224
export const AutogrowTextarea = (props: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) => {
const ref = useRef<HTMLTextAreaElement>(null);
let topPadding = 0;
let bottomPadding = 0;
const resize = () => {
ref.current.style.height = 'auto';
ref.current.style.height = ref.current.scrollHeight - topPadding - bottomPadding + 'px';
};
const delayedResize = () => {
window.setTimeout(resize, 0);
};
const getPropertyValue = (it: string) => {
return Number.parseFloat(window.getComputedStyle(ref.current).getPropertyValue(it));
};
useEffect(() => {
[topPadding, bottomPadding] = ['padding-top', 'padding-bottom'].map(getPropertyValue);
ref.current.focus();
ref.current.select();
resize();
}, []);
return <textarea ref={ref} onChange={resize} onCut={delayedResize} onPaste={delayedResize} onDrop={delayedResize} onKeyDown={delayedResize} rows={1} {...props} />;
};
import { useRef, useState } from "react"
const TextAreaComponent = () => {
const [inputVal, setInputVal] =useState("")
const inputRef = useRef(null)
const handleInputHeight = () => {
const scrollHeight = inputRef.current.scrollHeight;
inputRef.current.style.height = scrollHeight + "px";
};
const handleInputChange = () => {
setInputVal(inputRef.current.value)
handleInputHeight()
}
return (
<textarea
ref={inputRef}
value={inputVal}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit(e);
inputRef.current.style.height = "40px";
}
}}
/>
)}
Extremely simple solution:
function allowTextareasToDynamicallyResize() {
let textareas = document.getElementsByTagName('textarea');
for (let i = 0; i < textareas.length; i++) {
textareas[i].style.height = textareas[i].scrollHeight + 'px';
textareas[i].addEventListener('input', (e) => {
e.target.style.height = textareas[i].scrollHeight + 'px';
});
}
}
// Call this function in the componentDidMount() method of App,
// or whatever class you want that contains all the textareas
// you want to dynamically resize.
This works by setting an event listener to all textareas. For any given textarea, new input will trigger a function that resizes it. This function looks at any scrollHeight, i.e. the height that is overflowing out of your existing container. It then increments the textarea height by that exact height. Simple!
As mentioned in the comment, you have to call this function in some method, but the important part is that you call it AFTER everything is mounted in React / populated in JS. So the componentDidMount() in App is a good place for this.

Categories

Resources