useState not behaving as expected - reactjs

I have a problem with useState. editorDataOpen is updating correctly when set from openEditorData , but not when set from closeEditorData. The line console.log('Entering Profile > closeEditorData()') is reached without a problem.
The output I see in the console log is:
Render
Render
false (editorDataOpen set for the first time)
Render
Render
Render
Render
true (editorDataOpen set to true by a click, which opens Editor)
Entering Profile > closeEditorData() (triggered by a different click to close Editor, should set editorDataOpen to false, but doesn't)
Render
Render
and that's it, it never prints a last false, which would mean editorDataOpen is never set?
I've spent too much time on this today and I just cannot see where the bug is. Can someone help please? This is the code in question:
import React from 'react'
import {withTheme} from 'styled-components'
import {withContext} from '../../context/ContextHOC'
import withLoadingScreen from '../hocs/LoadingScreenHOC'
import {getEditorWidth} from '../../functions/funcEditor'
import {saveImagePlus} from '../../functions/funcDataSaver'
import Submodule from '../ui/Submodule'
import ImageCapsule from '../ui/ImageCapsule'
import EditorImage from '../editors/EditorImage'
import EditorProfileData from '../editors/EditorProfileData'
import Spacer from '../ui/Spacer'
import Table from '../ui/Table'
import ContainerGrid from '../ui/ContainerGrid'
import * as ops from '../../functions/funcStringMath'
import * as parse from '../../functions/funcDataParser'
const Profile = (props) => {
const s = props.theme.sizes
const c = props.theme.cards
const {setLoadingOn, setLoadingOff} = props
const [image, setImage] = React.useState(props.context.current.modules.me.profile.image)
const [editorImageOpen, setEditorImageOpen] = React.useState(false)
const [editorDataOpen, setEditorDataOpen] = React.useState(false)
const openEditorImage = () => setEditorImageOpen(true)
const openEditorData = () => setEditorDataOpen(true)
const closeEditorImage = () => {
setEditorImageOpen(false)
setLoadingOff()
}
const closeEditorData = () => {
console.log('Entering Profile > closeEditorData()')
setEditorDataOpen(false)
setLoadingOff()
}
React.useEffect(() => console.log(editorDataOpen), [editorDataOpen])
const updateAfterSavingImage = (img) => {
setImage({
url: img.url,
scale: img.scale,
position: img.position
})
closeEditorImage()
}
const handleImageChanged = (img) => {
if (img != undefined){
setLoadingOn()
const data = {
companyId: props.context.current.company.id,
userId: props.context.current.user.id,
routeFile: props.context.routes.meProfileImage,
routeData: props.context.routes.meProfileImageData,
}
saveImagePlus(img, data, updateAfterSavingImage)
}
else {
console.log('Error: Image received is undefined, cannot save.')
closeEditorImage()
}
}
const spacer =
<Spacer
width = '100%'
height = {s.spacing.default}
/>
const unparsedData = props.context.current.modules.me.profile.data
const parsedData = parse.profileData(props.context.current.modules.me.profile.data)
console.log('Render')
return(
<Submodule
isMobile = {c.cardsPerRow == 1 ? true : false}
header = {{
text: 'Profile',
}}
{...props}
>
<ImageCapsule
onClick = {openEditorImage}
id = {'container_imageprofile'}
width = '100%'
image = {image}
$nodrag
/>
{editorImageOpen &&
<EditorImage
open = {editorImageOpen}
closeSaving = {handleImageChanged}
closeWithoutSaving = {closeEditorImage}
image = {image}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Image',
}}
/>
}
{spacer}
{spacer}
<ContainerGrid
// bgcolor = '#C43939'
width = '100%'
justify = {s.justify.center}
onClick = {openEditorData}
$clickable
>
<Table
$nomouse
width = '100%'
data = {parsedData}
settings = {{
cell: {
padleft: s.spacing.default,
padright: s.spacing.default,
padtop: ops.round((ops.divide([s.spacing.default, 4]))),
padbottom: ops.round((ops.divide([s.spacing.default, 4]))),
},
columns: [
{type: 'defaultRight', width: '30%'},
{type: 'default', width: '70%'},
]
}}
/>
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
close = {closeEditorData}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
</ContainerGrid>
{spacer}
{spacer}
</Submodule>
)
}
export default (
withTheme(
withContext(
withLoadingScreen(
Profile
)
)
)
)
EDIT: This one has been solved by Dehan de Croos, many thanks!
So as Dehan mentions below, the event was bubbling up and triggering openEditorData in ContainerGrid. The whole thing was mistifying because editorImageOpen was working correctly while editorDataOpen was not, and they both do the same thing: open an Editor window.
Once Dehan solved the mistery, I realized that the differene between the 2 is that inside ImageCapsule there is a ClickLayer component, which is there just to catch the click and the callback.
I did not use a ClickLayer with ContainerGrid, and that is why the event was able to bubble up.
Following Dehan's advice, I solved just by adding a ClickLayer inside ContainerGrid, like this:
<ContainerGrid
// bgcolor = '#C43939'
width = '100%'
justify = {s.justify.center}
// onClick = {openEditorData}
$clickable
>
<ClickLayer
onClick = {openEditorData}
/>
<Table
$nomouse
width = '100%'
data = {parsedData}
settings = {{
cell: {
padleft: s.spacing.default,
padright: s.spacing.default,
padtop: ops.round((ops.divide([s.spacing.default, 4]))),
padbottom: ops.round((ops.divide([s.spacing.default, 4]))),
},
columns: [
{type: 'defaultRight', width: '30%'},
{type: 'default', width: '70%'},
]
}}
/>
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
close = {closeEditorData}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
</ContainerGrid>

It would be better to have a working code snippet to diagnose this, but there is a good chance that the event might be bubbling up the component tree and triggering the openEditorData event here.
<ContainerGrid
// bgcolor = '#C43939'
width = '100%'
justify = {s.justify.center}
onClick = {openEditorData}
$clickable
>
To check this quickly move the component shown below outside the <ContainerGrid ... /> component. And check whether this fixes things.
To clarify here, The move of code should happen so that <
EditorProfileData ... /> will never appear as a child component of
<ContainerGrid ... />
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
close = {closeEditorData}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
If now it's working properly and you desperately need to maintain the above component hierarchy/structure. You can call
stopPropegation but you will need to have the native JS event with you. To explain how to do this I need to know what <EditorProfileData ... /> looks like. But assuming the close prop does return the native click event as a callback, the fix will look something like this.
{editorDataOpen &&
<EditorProfileData
open = {editorDataOpen}
//close = {closeEditorData}
// If close provides the native event use following
close = { e => {
e.stopPropagation();
closeEditorData();
}}
width = {getEditorWidth(1, c.cardsPerRow, c.card.width, s.spacing.default)}
header = {{
text: 'Edit Profile Data',
}}
data = {unparsedData}
/>
}
If not we need to find the original onClick event and pass up to that prop callback.

Related

How to show only one topbar at a time?

I'm using a package called suneditor as a rich text editor. I'm only showing the toolbar on focus and hide in on blur. This works (more or less), but the problem is that I have several editors, so when I click from one editor to the other, the topbar doesn't disappear properly. I need to click in each one of them then click outside again to hide it. What am I doing wrong and how can I prevent this behaviour?
Here's how my code looks like
const [hideToolbar, setHideToolbar] = React.useState<boolean>(true)
const handleOnBlur = () => {
setHideToolbar(true)
}
return (
<StyleOverrides transparent={transparent} disable={disable}>
<SunEditor
autoFocus = {autoFocus}
height = '100%'
width = '100%'
resizeEnable = 'false'
onChange = {onChange}
onSave = {onSave}
onBlur = {handleOnBlur}
readOnly = {readOnly}
disable = {disable}
setContents = {value}
defaultValue = {defaultValue}
hideToolbar = {hideToolbar}
onImageUploadBefore = {onImageUploadBefore}
onImageUpload = {onImageUpload}
onFocus = {(event) => {
setHideToolbar(false)
}}
{...props}
/>
</StyleOverrides>
)
}

react functional component with ag grid cannot call parent function via context

I am using ag-grid-react and ag-grid-community version 22.1.1. My app is written using functional components and hooks. I have a cellrenderer component that is attempting to call a handler within the parent component using the example found here. Is this a bug in ag-grid? I have been working on this application for over a year as I learn React, and this is my last major blocker so any help or a place to go to get that help would be greatly appreciated.
Cell Renderer Component
import React from 'react';
import Button from '../../button/button';
const RowButtonRenderer = props => {
const editClickHandler = (props) => {
let d = props.data;
console.log(d);
props.context.foo({d});
//props.editClicked(props);
}
const deleteClickHandler = (props) => {
props.deleteClicked(props);
}
return (<span>
<Button classname={'rowbuttons'} onClick={() => { editClickHandler(props) }} caption={'Edit'} />
<Button classname={'rowbuttons'} onClick={() => { deleteClickHandler(props) }} caption={'Delete'} />
</span>);
};
export default RowButtonRenderer;
Parent Component
function Checking() {
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: (props) => foo(props)};
const gridOptions = (params) => {
if (params.node.rowIndex % 2 === 0) {
return { background: "#ACC0C6" };
}
};
const frameworkComponents = {
rowButtonRenderer: RowButtonRenderer,
};
.
.
.
return (
<>
<AgGridReact
getRowStyle={gridOptions}
frameworkComponents={frameworkComponents}
context = {context}
columnDefs={columnDefinitions}
rowData={rowData}
headerHeight="50"
rowClass="gridFont"
></AgGridReact>
</>
);
}
When clicking the edit button on a row, the debugger says that there is a function.
This error is received though:
You are passing the context object in this code section:
const context = {componentParent: (props) => foo(props)};
...
<AgGridReact
context={context}
{...}
></AgGridReact>
And in your cell renderer you call this
props.context.foo({d});
While it should be this
props.context.componentParent({d});
Also you can assign your callback directly since it receives the same parameter and returns the same result (if any)
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: foo};
You can also use this shorthand syntax from ES6 when assigning object property
function componentParent(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent};
Live Demo

Cannot crop image using react-image-crop

I trying to crop image using react-image-crop (https://www.npmjs.com/package/react-image-crop/v/6.0.7). Actually I cannot move cropped area or stretch it. I also use there React DragZone to upload image. Could you explain what I'm doing wrong and what could be the problem ? React-image-crop css is also imported. Can anybody help?
import React, {useCallback} from 'react'
import {useDropzone} from 'react-dropzone'
import ReactCrop from 'react-image-crop'
import ReactDOM from "react-dom";
import 'react-image-crop/dist/ReactCrop.css';
import "../Styles/ImageUpload.css"
function ImageUpload(files, addFile) {
const crop = {
disabled: false,
locked: false,
unit: 'px', // default, can be 'px' or '%'
x: 130,
y: 50,
width: 200,
height: 200
}
const onCrop = (image, pixelCrop, fileName) => {
}
const onImageLoaded = (image) => {
}
const onCropComplete = async crop => {
}
const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
if (Object.keys(rejectedFiles).length !== 0) {
}
else {
files.addFile(acceptedFiles);
let popupBody = document.getElementsByClassName("popup-container")[0];
popupBody.innerHTML = "";
let cropElement = (<ReactCrop src = {URL.createObjectURL(acceptedFiles[0])} crop={crop}
onImageLoaded={onImageLoaded}
onComplete={onCropComplete}
onChange={onCrop} />);
ReactDOM.render(cropElement, popupBody);
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop, noKeyboard: true})
return (
<div className = "popup-container">
<p>Upload new photo</p>
{
(() => {
if (files.length == undefined) {
return (<div>
<input className = "testinput" {...getInputProps()} />
<div className = "popup-body" {...getRootProps()}>
<img src={require("../Resources/upload-image.png")} className = "image-upload-img"/>
<p style = {{'font-family':'Verdana'}}>Chouse a <b className = "file-manualy-
upload">file</b> or drag it here</p>
</div> </div>)
}
else {
}
})()}
</div>
)
}
I also face the same issue, Here you are trying to add disabled, locked in crop property but it is invalid because crop property takes following values:
{
unit: 'px', // default, can be 'px' or '%'
x: 130,
y: 50,
width: 200,
height: 200
}
To solve this add disabled, locked as a props in component instead of crop property.
I'm not sure sure why you would do a ReactDom.render from within the function. It seems to be counter to what React is and guessing that the library isn't binding events properly.
I would suggest, after the user drops the image, you load the image using a file reader and set it as state and change the display to show the <ReactCrop src=={this.state.img} />. The crop also needs to be in a state, so that when it changes, it updates the crop rectangle.
Here's a working example from the creator himself: https://codesandbox.io/s/72py4jlll6

Images Rerendering inside Styled Component when Chrome Dev Tools is open

This is a bit of a strange one and not sure why it's happening exactly.
When the component mounts, I call a function that in my application makes an HTTP request to get an array of Objects. Then I update 3 states within a map method.
enquiries - Which is just the response from the HTTP request
activeProperty - Which defines which object id is current active
channelDetails - parses some of the response data to be used as a prop to pass down to a child component.
const [enquiries, setEnquiries] = useState({ loading: true });
const [activeProperty, setActiveProperty] = useState();
const [channelDetails, setChannelDetails] = useState([]);
const getChannels = async () => {
// In my actual project,this is an http request and I filter responses
const response = await Enquiries;
const channelDetailsCopy = [...channelDetails];
setEnquiries(
response.map((e, i) => {
const { property } = e;
if (property) {
const { id } = property;
let tempActiveProperty;
if (i === 0 && !activeProperty) {
tempActiveProperty = id;
setActiveProperty(tempActiveProperty);
}
}
channelDetailsCopy.push(getChannelDetails(e));
return e;
})
);
setChannelDetails(channelDetailsCopy);
};
useEffect(() => {
getChannels();
}, []);
Then I return a child component ChannelList that uses styled components to add styles to the element and renders child elements.
const ChannelList = ({ children, listHeight }) => {
const ChannelListDiv = styled.div`
height: ${listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
return <ChannelListDiv className={"ChannelList"}>{children}</ChannelListDiv>;
};
Inside ChannelList component I map over the enquiries state and render the ChannelListItem component which has an assigned key on the index of the object within the array, and accepts the channelDetails state and an onClick handler.
return (
<>
{enquiries &&
enquiries.length > 0 &&
!enquiries.loading &&
channelDetails.length > 0 ? (
<ChannelList listHeight={"380px"}>
{enquiries.map((enquiry, i) => {
return (
<ChannelListItem
key={i}
details={channelDetails[i]}
activeProperty={activeProperty}
setActiveProperty={id => setActiveProperty(id)}
/>
);
})}
</ChannelList>
) : (
"loading..."
)}
</>
);
In the ChannelListItem component I render two images from the details prop based on the channelDetails state
const ChannelListItem = ({ details, setActiveProperty, activeProperty }) => {
const handleClick = () => {
setActiveProperty(details.propId);
};
return (
<div onClick={() => handleClick()} className={`ChannelListItem`}>
<div className={"ChannelListItemAvatarHeads"}>
<div
className={
"ChannelListItemAvatarHeads-prop ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.propertyImage})`
}}
/>
<div
className={
"ChannelListItemAvatarHeads-agent ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.receiverLogo})`
}}
/>
</div>
{activeProperty === details.propId ? <div>active</div> : null}
</div>
);
};
Now, the issue comes whenever the chrome dev tools window is open and you click on the different ChannelListItems the images blink/rerender. I had thought that the diff algorithm would have kicked in here and not rerendered the images as they are the same images?
But it seems that styled-components adds a new class every time you click on a ChannelListItem, so it rerenders the image. But ONLY when the develop tools window is open?
Why is this? Is there a way around this?
I can use inline styles instead of styled-components and it works as expected, though I wanted to see if there was a way around this without removing styled-components
I have a CODESANDBOX to check for yourselves
If you re-activate cache in devtool on network tab the issue disappear.
So the question becomes why the browser refetch the image when cache is disabled ;)
It is simply because the dom change so browser re-render it as you mentioned it the class change.
So the class change because the componetn change.
You create a new component at every render.
A simple fix:
import React from "react";
import styled from "styled-components";
const ChannelListDiv = styled.div`
height: ${props => props.listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
const ChannelList = ({ children, listHeight }) => {
return <ChannelListDiv listHeight={listHeight} className={"ChannelList"}>{children}</ChannelListDiv>;
};
export default ChannelList;
I think it has to do with this setting to disable cache (see red marking in image)
Hope this helps.

React dim element that is rendered from a map

I am working on showing images in a card using React. The images come from a API and are resolved when rendering the images page. Now I want to use the Dimmer component from the React Semantic-UI design library and dim an image on a mouseover. I tried the following example from the document page:
const dimmedID = this.state.dimmedID
const collections = this.state.collections
const collectionsList = collections.map((project) =>
<Dimmer.Dimmable
key = { project.id }
as = {Image}
dimmed = {dimmedID === project.id ? true : false}
dimmer = {{ active, content }}
onMouseEnter = {() => { this.handleShow(project.id) }}
onMouseLeave = {() => { this.handleShow('') }}
src = { project.thumbnail }
/>
)
When triggering the onMouseEnter, the dimmedID state object is set to the id of the image. However, all images that are rendered are being dimmed. Not the image which the mouse is on. I tried with a shorthand if-else on the dimmed parameter but that does not seem to work. When hovering with the mouse over one image, all images get dimmed.
Does someone know how to fix this?
Ok so apparently the fix is easy... so much for reading...
const dimmedID = this.state.dimmedID
const collections = this.state.collections
const collectionsList = collections.map((project) =>
<Dimmer.Dimmable
key = { project.id }
as = {Image}
dimmed = {dimmedID === project.id ? true : false}
dimmer = {{ active: dimmedID === project.id, content }}
onMouseEnter = {() => { this.handleShow(project.id) }}
onMouseLeave = {() => { this.handleShow('') }}
src = { project.thumbnail }
/>
)

Resources