useRef() returning 'undefined' when used with custom hook on initial render when using filter() - reactjs

I have an image slider component and a simple custom hook that gets the refElement and the width of the element using the useRef hook. -
The code sandbox is here Image Slider
When I use the slider component and just map the data in without filtering, everything works fine. If I filter and map the data then I get Uncaught TypeError: elementRef.current is undefined . (In the sandbox you have to comment out the second instance (unfiltered) of SliderTwo to recreate the error. Why does it work without the filter but not with (when rendered by itself)? More in depth explanation below.
useSizeElement()
import { useState, useRef, useEffect } from 'react';
const useSizeElement = () => {
const [width, setWidth] = useState(0);
const elementRef = useRef();
useEffect(() => {
setWidth(elementRef.current.clientWidth); // This will give us the width of the element
}, [elementRef.current]);
return { width, elementRef };
};
export default useSizeElement;
I call the hook (useSizeElement) inside of a context because I need the width to use in another hook in a different component thus:
context
import React, { createContext, useState, useEffect} from 'react';
import useSizeElement from '../components/flix-slider/useSizeElement';
export const SliderContext = createContext();
export const SliderProvider = ({children}) => {
const { width, elementRef } = useSizeElement();
const [currentSlide, setCurrentSlide] = useState();
const [isOpen, setIsOpen] = useState(false)
console.log('context - width', width, 'elementRef', elementRef)
const showDetailsHandler = movie => {
setCurrentSlide(movie);
setIsOpen(true)
};
const closeDetailsHandler = () => {
setCurrentSlide(null);
setIsOpen(false)
};
const value = {
onShowDetails: showDetailsHandler,
onHideDetails: closeDetailsHandler,
elementRef,
currentSlide,
width,
isOpen
};
return <SliderContext.Provider value={value}>{children}</SliderContext.Provider>
}
I get the width of the component from the elementRef that was passed from the context.-
Item Component
import React, { Fragment, useContext } from 'react';
import { SliderContext } from '../../store/SliderContext.context';
import ShowDetailsButton from './ShowDetailsButton';
import Mark from './Mark';
import { ItemContainer } from './item.styles';
const Item = ({ show }) => {
const { onShowDetails, currentSlide, isOpen, elementRef } =
useContext(SliderContext);
const isActive = currentSlide && currentSlide.id === show.id;
return (
<Fragment>
<ItemContainer
className={isOpen ? 'open' : null}
ref={elementRef}
isActive={isActive}
isOpen={isOpen}
>
<img
src={show.thumbnail.regular.medium}
alt={`Show title: ${show.title}`}
/>
<ShowDetailsButton onClick={() => onShowDetails(show)} />
</ItemContainer>
</Fragment>
);
};
export default Item;
The width is passed using context where another hook is called in the Slider Component:
Slide Component
import useSizeElement from './useSizeElement';
import { OuterContainer } from './SliderTwo.styles';
const SliderTwo = ({ children }) => {
const {currentSlide, onHideDetails, isOpen, width, elementRef } = useContext(SliderContext);
const { handlePrev, handleNext, slideProps, containerRef, hasNext, hasPrev } =
useSliding( width, React.Children.count(children));
return (
<Fragment>
<SliderWrapper>
<OuterContainer isOpen={isOpen}>
<div ref={containerRef} {...slideProps}>
{children}
</div>
</OuterContainer>
{hasPrev && <SlideButton showLeft={hasPrev} onClick={handlePrev} type="prev" />}
{hasNext && <SlideButton showRight={hasNext} onClick={handleNext} type="next" />}
</SliderWrapper>
{currentSlide && <Content show={currentSlide} onClose={onHideDetails} />}
</Fragment>
);
};
export default SliderTwo;
Now everything works fine if I just map the data with no filters into the slider as shown in the sandbox. But if I apply a filter to display only what I want I get -
Uncaught TypeError: elementRef.current is undefined
I do know that you can't create a ref on an element that does not yet exist and I've seen examples where you can use useEffect to get around it but I can't find the solution to get it to work.
Here is the App.js - To see the error I'm getting, comment out the second instance of . As long as I'm running one instance without filtering the data, it works, but it won't work by itself.
import { useState, useEffect, Fragment } from "react";
import SliderTwo from "./components/SliderTwo";
import Item from "./components/Item";
import shows from "./data.json";
import "./App.css";
function App() {
const [data, setData] = useState(null);
const datafunc = () => {
let filteredData = shows.filter((show) => {
if (show.isTrending === true) {
return show;
}
});
setData(filteredData);
};
useEffect(() => {
datafunc();
}, []);
console.log("Trending movies", data);
return (
<Fragment>
<div className="testDiv">
{shows && data && (
<SliderTwo>
{data && data.map((show) => <Item show={show} key={show.id} />)}
</SliderTwo>
)}
</div>
<div className="testDiv">
<SliderTwo>
{shows.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div>
</Fragment>
);
}
export default App;
Full code: Sandbox - https://codesandbox.io/s/twilight-sound-xqglgk

I think it may be an issue when the useSizeElement is first mounted as the useEffect will run once at the beginning of each render.
When it runs at the first instance and the ref is not yet defined so it was returning the error: Cannot read properties of undefined (reading 'clientWidth')
If you modify your code to this I believe it should work:
import { useState, useRef, useEffect } from "react";
const useSizeElement = () => {
const [width, setWidth] = useState(0);
const elementRef = useRef();
useEffect(() => {
if (elementRef.current) setWidth(elementRef.current.clientWidth); //
This will give us the width of the element
}, [elementRef]);
return { width, elementRef };
};
export default useSizeElement;
This way you are checking if the elementRef is defined first before setting the width
UPDATE:
<Fragment>
<div className="testDiv">
<SliderTwo>
{shows
.filter((show) => {
if (show.isTrending === true) {
return show;
}
return false;
})
.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div>
{/* <div className="testDiv">
<SliderTwo>
{shows.map((show) => (
<Item show={show} key={show.id} />
))}
</SliderTwo>
</div> */}
</Fragment>

Related

search filter in React giving error while trying to use state hooks

I am try to add search feature to an existing lists of robot names.
In order to do so I am trying to useState hooks. I have an App component and Header component which has the input tag for search field.
Error I am getting is 'InputEvent' is assigned a value but never used.
Below is the code for App component (main component).
import "./App.css";
import Header from "./Header";
import Robo from "./Robo";
import { robots } from "./robots";
import { useState } from "react";
function App() {
const [query, setQuery] = useState("");
const InputEvent = (e) => {
const data = e.target.value;
setQuery(data);
const extraction = robots
.filter((curElem, index) =>
robots[index].name.toLowerCase().includes(query)
)
.map((curElem, index) => {
return (
<Robo
key={robots[index].id}
id={robots[index].id}
name={robots[index].name}
email={robots[index].email}
/>
);
});
return (
<div className="App">
<Header query={query} InputEvent={InputEvent} />
<div className="robo-friends-container">{extraction};</div>
</div>
);
};
}
export default App;
Child component
import React from "react";
import "./header.css";
const Header = ({ query, InputEvent }) => {
return (
<>
<div className="headerText">ROBO FRIENDS</div>
<div>
<input
type="text"
id="lname"
name="lname"
placeholder="Search"
value={query}
onChange={InputEvent}
/>
</div>
</>
);
};
export default Header;
Here is my answer in stackblitz app
https://stackblitz.com/edit/stackoverflow-robots-filter?file=App.tsx,Robo.tsx,Header.tsx,robots.ts
I have altered the code a bit.. you can fork the project and play with it..
You can add debounce option to your input, which prevents unwanted re-renders
Adding the changes:
function App() {
const [query, setQuery] = useState(undefined);
const [filteredRobots, setFilteredRobots] = useState([]);
useEffect(() => {
console.log(query);
const filteredRobots = robots.filter((robot) => {
return robot.name.includes(query);
});
if (filteredRobots.length) {
setFilteredRobots(filteredRobots);
}
}, [query]);
const onQueryChange = (e) => {
const data = e.target.value;
setQuery(data);
};
const renderRobots = () => {
if (!query || !query.length) {
return <p>{'Search to find Robots'}</p>;
}
if (filteredRobots && filteredRobots.length && query && query.length) {
return filteredRobots.map((filteredRobot) => (
<Robo
key={filteredRobot.id} //id is unique key in your data
name={filteredRobot.name}
id={filteredRobot.id}
email={filteredRobot.email}
/>
));
}
return <p>{'No Robots Found'}</p>;
};
return (
<div className="App">
<Header query={query} InputEvent={onQueryChange} />
{renderRobots()}
</div>
);
}
Problems in your code:
Const InputChange is a function that can be used as prop for any React component .. but you have added InputChange inside the InputChange named function itself which is incorrect
Extraction is a jsx variable which is created from Array.filter.. on each item, filter passes a item[index] to the filter function.. you dont want to do robots[index].name.toLowerCase().includes(query).. instead you could have done curElem.name.toLowerCase().includes(query) and same applies for Array.map

Destructed props sent to child component returning undefined

I'm a bit lost here. I've done this a bunch of time and have never had this issue before. I'm passing a boolean state to a modal component. I followed the code from the parent and it is set properly but as soon as it gets to the modal it returns as undefined.
Here is the parent:
import React, { useEffect, Fragment, useState } from 'react'
import './styles.css'
import LandingPageModal from './LandingPageModal'
import { testImages } from './testData'
const LandingPage = () => {
const [images, setImages] = useState([])
const [renderImages, setRenderImages] = useState(false)
const [showModal, setShowModal] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
useEffect(() => {
setImages(testImages)
setShowModal(true)
setIsLoaded(true)
}, [])
useEffect(() => {
if (images && images.length > 0) {
setRenderImages(true)
}
}, [images])
const FeaturedUsers = () => {
return (
renderImages ?
<Fragment>
<div className='grid'>
{images.map((image) => (
<img src={`/images/${image.src}`} alt={image.caption} />
))}
</div>
</Fragment> : ''
)
}
return(
isLoaded ?
<Fragment>
<FeaturedUsers />
<LandingPageModal show={showModal} />
</Fragment> : ''
)
}
export default LandingPage
and here is the modal:
import React, { useState, useEffect } from 'react'
import ReactModal from 'react-modal'
import './styles.css'
const LandingPageModal = ({ showModal }) => {
const [isModalOpen, setIsModalOpen] = useState(showModal)
console.log('Is Show: ' + showModal)
return (
<ReactModal
isOpen={isModalOpen}
>
<div className='main-wrapper'>
<div className='text'>
<p>
<strong>Welcome</strong>
<br />
<br />
Please sign in or sign up
</p>
</div>
</div>
</ReactModal>
)
}
export default LandingPageModal
In the LandingPage component, you accidentally renamed showModal to show.

Why am I getting a null value in localStorage even after parsing it?

I'm trying to build a crypto tracker where you can add the items by clicking a button. Each time the button is clicked, the array should be added to the storage with the name "crypto" and then on another component where it is the portfolio one we should be able to get the items.
Here is where I set the item to an array whenever I click the add button:
import React, {useEffect, useState} from 'react'
import axios from 'axios'
import './tracker.css'
import Navigation from './Nav.js'
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
function Tracker() {
const [data, setData] = useState([])
const [portfolio, setPortfolio] = useState([])
useEffect(() => {
setInterval(() => {
const fetchData = async () => {
const result = await axios('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false' , {
'mode': 'no-cors',
'headers': {
'Access-Control-Allow-Origin': '*',
}
})
setData(result.data)
}
fetchData()
}, 1000)
}, [])
return (
<div>
<Navigation />
<div className="tracker__names">
<b>Coins</b>
<b>Symbol</b>
<b>Price</b>
<b>Market Cap</b>
</div>
{data.map((coins, i) => {
const addToPortfolio = () => {
setPortfolio([...portfolio, data[i]])
localStorage.setItem('crpyto', JSON.stringify(portfolio))
}
return (
<>
<div className="tracker__main">
<div className="tracker__img">
<img src={coins.image} className="tracker__image"/>
<button key={i} onClick={addToPortfolio}>{coins.id}</button>
</div>
<div className="tracker__symbol">
<p>{coins.symbol}</p>
</div>
<div className="tracker__price">
<p></p>
${coins.current_price}
</div>
<div className="tracker__market">
<p></p>
${coins.market_cap}
</div>
</div>
</>
)
})}
</div>
)
}
export default Tracker
Here is the component where I want to get the item:
import React, {useState, useEffect} from 'react'
import Navigation from './Nav.js'
function Portfolio() {
const [value, setValue] = useState(JSON.parse(localStorage.getItem('crypto')) || '')
useEffect(() => {
console.log(value)
}, )
return (
<div>
<Navigation />
{value}
</div>
)
}
export default Portfolio
It is because useState is executed before JSON.parse(localStorage.getItem('crypto')) and once you get the value from the localstorage, component doesn't re-render.
Instead do:
useEffect(() => {
const crypto = JSON.parse(localStorage.getItem('crypto'))
if(crypto) setValue(crypto)
}, [])
In React you can't set a state var and on the next line save it in localStorage (or even read it). This because setPortfolio is async!
To solve this you have I think 2 ways:
store value and not state variable:
localStorage.setItem('crpyto', JSON.stringify([...portfolio, data[i]]))
use an useEffect hook:
useEffect(() => {
localStorage.setItem('crpyto', JSON.stringify(portfolio))
}, [portfolio])
First of all, when yo uare setting state like this, in the next block of code, portfolio won't necessarily have the updated state.
setPortfolio([...portfolio, data[i]])
localStorage.setItem('crpyto', JSON.stringify(portfolio))
update the portfolio like this.
const newPortfolio = [...portfolio, data[i]];
setPortfolio(newPortfolio )
localStorage.setItem('crpyto', JSON.stringify(newPortfolio))

React complains element type is invalid when trying to use context

I'm trying to use React Context to update navbar title dynamically from other child components. I created NavbarContext.js as follows. I have wrapped AdminLayout with NavContext.Provider and use useContext in Course.js to dynamically update navbar title inside useEffect. However, when I'm doing this, react throws the following error on the screen.
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
How can I use context properly so that I can update Header title from Course.js inside its useEffect?
NavbarContext.js
import React, {useState} from 'react'
export default () => {
const [name,setName] = useState("")
const NavContext = React.createContext({
name: "",
changeName: name => setName(name)
})
const NavProvider = NavContext.Provider
const NavConsumer = NavContext.Consumer
return NavContext
}
AdminLayout.js
<NavContext.Provider>
<div className={classes.wrapper}>
<Sidebar
routes={routes}
logoText={"Widubima"}
logo={logo}
image={image}
handleDrawerToggle={handleDrawerToggle}
open={mobileOpen}
color={color}
{...rest}
/>
<div className={classes.mainPanel} ref={mainPanel}>
<Navbar
routes={routes}
handleDrawerToggle={handleDrawerToggle}
{...rest}
/>
{/* On the /maps route we want the map to be on full screen - this is not possible if the content and conatiner classes are present because they have some paddings which would make the map smaller */}
{getRoute() ? (
<div className={classes.content}>
<div className={classes.container}>{switchRoutes}</div>
</div>
) : (
<div className={classes.map}>{switchRoutes}</div>
)}
</div>
</div>
</NavContext.Provider>
Navbar.js
import NavContext from "context/NavbarContext"
export default function Header(props) {
function makeBrand() {
var name;
props.routes.map(prop => {
if (window.location.href.indexOf(prop.layout + prop.path) !== -1) {
name = prop.name;
document.title = name;
}
return null;
});
return name;
}
return (
<AppBar className={classes.appBar + appBarClasses}>
<Toolbar className={classes.container}>
<div className={classes.flex}>
{/* Here we create navbar brand, based on route name */}
<NavContext.Consumer>
{({ name, setName }) => (
<Button
color="transparent"
href="#"
className={classes.title}
style={{ fontSize: "1.5em", marginLeft: "-2%" }}
>
{makeBrand() || name}
</Button>
)}
</NavContext.Consumer>
</Toolbar>
</AppBar>
);
}
Course.js
import React, { useState, useEffect, useContext } from "react";
import NavContext from "context/NavbarContext"
const AdminCourse = props => {
const context = useContext(NavContext);
useEffect(() => {
Axios.get('/courses/'+props.match.params.courseId).then(
res => {
context.changeName("hello")
}
).catch(err => {
console.log(err)
})
return () => {
setCourseId("");
};
});
return (
<GridContainer>
</GridContainer>
);
};
export default AdminCourse;
i think problem is there with your NavbarContext.js.
you are not exporting NavContext also.
you are defining provider, consumer but you are not using them either.
here's how you can solve your problem.
first create context and it's provider in a file as following.
NavContext.js
import React, { useState } from "react";
const NavContext = React.createContext();
const NavProvider = props => {
const [name, setName] = useState("");
let hookObject = {
name: name,
changeName: setName
};
return (
<NavContext.Provider value={hookObject}>
{props.children}
</NavContext.Provider>
);
};
export { NavProvider, NavContext };
in above code first i am creating context with empty value.
the i am creating NavProvider which actually contains value name as a state hook inside it.hookObject exposes state as per your naming conventions in code.
now i for testing purpose i defined two consumers.
one is where we update name in useEffect, that is ,
ConsumerThatUpdates.js
import React, { useContext, useEffect } from "react";
import { NavContext } from "./NavContext";
const ConsumerThatUpdates = () => {
const { changeName } = useContext(NavContext);
useEffect(() => {
changeName("NEW NAME");
}, [changeName]);
return <div>i update on my useeffect</div>;
};
export default ConsumerThatUpdates;
you can update useEffect as per your needs.
another is where we use the name,
ConsumerThatDisplays.js
import React, { useContext } from "react";
import { NavContext } from "./NavContext";
const ConsumerThatDisplays = () => {
const { name } = useContext(NavContext);
return <div>{name}</div>;
};
export default ConsumerThatDisplays;
and finally my App.js looks like this,
App.js
import React from "react";
import "./styles.css";
import { NavProvider } from "./NavContext";
import ConsumerThatDisplays from "./ConsumerThatDisplays";
import ConsumerThatUpdates from "./ConsumerThatUpdates";
export default function App() {
return (
<div className="App">
<NavProvider>
<ConsumerThatDisplays />
<ConsumerThatUpdates />
</NavProvider>
</div>
);
}
hope this helps!!
if you want to know more about how to use context effectively, i recooHow to use React Context effectively

My state changes, but does not add class when useEffect, when I scroll

I need to change the background of a JSX element when the page goes down by 320 px, all with useEffect and useState. So far I managed to change the state, but does not add background class of another color.
I am using NODE 8.9.3, NPM 5.5.1 and REACT JS 16.9.0
import React, { useEffect, useState } from 'react'
import { useScrollYPosition } from 'react-use-scroll-position'
import { Container } from '../../styles/Container'
import { ContainerCustom, HeaderComp } from './styles'
import Logo from './Logo'
import Menu from './Menu'
import Icons from './Icons'
const ContainerBox = () => {
return (
<ContainerCustom fluid>
<Container>
<HeaderComp>
<Logo />
<Menu />
<Icons />
</HeaderComp>
</Container>
</ContainerCustom>
)
}
const Header = () => {
const [back, setBack] = useState(0)
const handleBackState = () => {
const scrollY = window.scrollY
if (scrollY > 320) {
setBack(1)
console.log(`Estado: ${back}`)
} else {
setBack(0)
console.log(`Estado após remover: ${back}`)
}
}
useEffect(() => {
window.addEventListener('scroll', handleBackState)
return () => {
window.removeEventListener('scroll', handleBackState)
}
}, [handleBackState])
return <ContainerBox className={back === 1 ? 'removeGradients' : ''} />
}
On console has the output State: 0, and after 320, State after remove:
1
Not every component also has a representation in the DOM. You need to apply the className to a component that actually has a corresponding DOM element to have your styles take any effect:
// className will not affect the DOM as this component does not render a DOM element
const WrappingComponent = ({className}) => (
<WrappedComponent className={className} />
);
// this className will be applied to the div in the DOM
const WrappedComponent = ({className}) => (
<div className={className}>Content here</div>
);

Resources