I have the following code:
import React, {useState} from 'React';
import Header from './styles.js';
const HeaderComponent = props =>
{
const [navBg, setNavBg] = useState(false);
const isHome = props.name === 'Homepage' ? true : false;
const changeNavBg = () =>
{
window.scrollY >= 800 ? setNavBg(true) : setNavBg(false);
}
window.addEventListener('scroll', changeNavBg);
return (
<Header {...(isHome && navBg ? { backgroundColor: '#00008' : {})} />
)
}
What I am trying to achieve is that when scrolling past 800px, I want my Header to change colors.
Cheers for your time.
Here's a couple of approaches you could try
1. Use the React onScroll UI event
return (
<div onScroll={changeNavBg}>
<Header {...(isHome && navBg ? { backgroundColor: '#00008' : {})} />
</div>
)
2. Consider binding the listener to a useEffect
import React, {useState} from 'React';
import Header from './styles.js';
const HeaderComponent = props => {
const [navBg, setNavBg] = useState(false);
const isHome = props.name === 'Homepage' ? true : false;
const changeNavBg = () => {
window.scrollY >= 800 ? setNavBg(true) : setNavBg(false);
}
useEffect(() => {
window.addEventListener('scroll', changeNavBg);
return () => {
window.removeEventListener('scroll', changeNavBg);
}
}, [])
return (
<Header {...(isHome && navBg ? { backgroundColor: '#00008' : {})} />
)
}
Related
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>
);
}
}
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]}
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.
I just integrated a light/dark mode toggle into my Gatsby site here. I based it off of Josh Comeau's article, and it works just fine in Chrome. However on the homepage when using Safari, when I click the toggle button the background color doesn't change unless I resize the window. Here is my gatsby-ssr.js:
import React from 'react';
import { THEME_COLORS } from 'utils/theme-colors';
import { LOCAL_STORAGE_THEME_KEY } from './src/contexts/ThemeContext';
const SetTheme = () => {
let SetThemeScript = `
(function() {
function getInitialTheme() {
const persistedColorPreference = window.localStorage.getItem('${LOCAL_STORAGE_THEME_KEY}');
const hasPersistedPreference = typeof persistedColorPreference === 'string';
if (hasPersistedPreference) {
return persistedColorPreference;
}
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const hasMediaQueryPreference = typeof mql.matches === 'boolean';
if (hasMediaQueryPreference) {
return mql.matches ? 'dark' : 'light';
}
return 'light';
}
const colorMode = getInitialTheme();
const root = document.documentElement;
root.style.setProperty(
'--color-primary',
colorMode === 'dark'
? '${THEME_COLORS.dark}'
: '${THEME_COLORS.light}'
);
root.style.setProperty(
'--color-secondary',
colorMode === 'dark'
? '${THEME_COLORS.light}'
: '${THEME_COLORS.dark}'
);
root.style.setProperty(
'--color-accent',
colorMode === 'dark'
? '${THEME_COLORS.accentLight}'
: '${THEME_COLORS.accentDark}'
);
root.style.setProperty('--initial-color-mode', colorMode);
})()`;
return <script id="theme-hydration" dangerouslySetInnerHTML={{ __html: SetThemeScript }} />;
};
export const onRenderBody = ({ setPreBodyComponents }) => {
setPreBodyComponents(<SetTheme />);
};
and my ThemeToggle component:
import React, { useContext } from 'react';
import { ThemeContext } from 'contexts/ThemeContext';
import { DarkModeSwitch } from 'react-toggle-dark-mode';
import { THEME_COLORS } from 'utils/theme-colors';
import s from './ThemeToggle.scss';
export const ThemeToggle = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
const toggleDarkMode = (checked: boolean) => {
toggleTheme(checked ? 'dark' : 'light');
};
return (
<div className={s.toggler}>
<DarkModeSwitch
checked={theme === 'dark'}
onChange={toggleDarkMode}
size={20}
sunColor={THEME_COLORS.dark}
moonColor={THEME_COLORS.light}
/>
</div>
);
};
Any ideas on how to fix this?
It doesn't appear that the toggleTheme property being destructured from the ThemeContext value triggers a re-render, but resizing your browser window does. Josh handles this by providing a setter function with a side effect (it manipulates the root styles directly):
const contextValue = React.useMemo(() => {
function setColorMode(newValue) {
const root = window.document.documentElement;
localStorage.setItem(COLOR_MODE_KEY, newValue);
Object.entries(COLORS).forEach(([name, colorByTheme]) => {
const cssVarName = `--color-${name}`;
root.style.setProperty(cssVarName, colorByTheme[newValue]);
});
rawSetColorMode(newValue);
}
return {
colorMode,
setColorMode,
};
}, [colorMode, rawSetColorMode]);
Here is the carousel I am using: react-slick
I want to be able to scroll through each slide using the mouse scroll up or down event.
Scroll up to increment, scroll down to decrement.
Found an example online of exactly what I need - just unsure of how to convert this into a react solution.
Example: https://codepen.io/Grawl/pen/mMLQQb
What would be the best way to achieve this in a "react" component based approach?
Here is my react component:
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import ReactSVG from 'react-svg';
import Slider from 'react-slick';
import MobileSVG from '../../../assets/svg/icons/Mobile_Icon_Option2.svg';
import TabletSVG from '../../../assets/svg/icons/Tablet_Icon_Option2.svg';
import DesktopSVG from '../../../assets/svg/icons/Desktop_Icon_Option2.svg';
const deviceIcons = {'mobile': MobileSVG, 'tablet': TabletSVG, 'desktop': DesktopSVG};
import BackToTopButton from '../BackToTopButton';
export default class ProductComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
const {productData} = this.props
//Slider settings
const settings = {
dots: true,
infinite: false,
speed: 500,
fade: true,
arrows: false,
centerMode: true,
slidesToShow: 1,
slidesToScroll: 1
}
//Slider items
const sliderItems = productData.map((obj, i) => {
return (
<div className="product-component row" key={i}>
<div className="product-component-image-wrap col-xs-12 col-sm-8">
<span className="product-heading">{obj.category}</span>
<div className="product-detail-wrap">
<img className="product-component-image" src={`${process.env.DB_URL}${obj.image}`} />
<ul className="list-device-support">
{obj.categoryDeviceSupport.map((obj, i) => {
return (<li key={i}>
<span className="svg-icon">
<ReactSVG path={deviceIcons[obj.value]} />
</span>
<span className="product-label">{obj.label}</span>
</li>)
})}
</ul>
</div>
</div>
<div className="product-component-info col-xs-12 col-sm-3">
<span className="align-bottom">{obj.title}</span>
<p className="align-bottom">{obj.categoryBrief}</p>
</div>
</div>
)
});
return (
<div className="product-component-wrap col-xs-12">
<Slider {...settings}>
{sliderItems}
</Slider>
<BackToTopButton scrollStepInPx="50" delayInMs="7" />
</div>
)
}
}
ProductComponent.propTypes = {
productData: PropTypes.array
};
ProductComponent.defaultProps = {
productData: []
};
You'd wanna do something like this:
constructor(props){
super(props);
this.slide = this.slide.bind(this);
}
slide(y){
y > 0 ? (
this.slider.slickNext()
) : (
this.slider.slickPrev()
)
}
componentWillMount(){
window.addEventListener('wheel', (e) => {
this.slide(e.wheelDelta);
})
}
render(){...
and add a ref to your slider:
<Slider ref={slider => this.slider = slider }>
So when the y value of the wheel event is greater than 0 i.e. scroll up then show next slide, when scrolling down show previous.
The following should work fine for you:
componentDidMount(){
let slickListDiv = document.getElementsByClassName('slick-list')[0]
slickListDiv.addEventListener('wheel', event => {
event.preventDefault()
event.deltaY > 0 ? this.slider.slickNext() : this.slider.slickPrev()
})
}
You should initialize the component like this:
<Slider {...settings} ref={slider => this.slider = slider.innerSlider}>
...
</Slider>
I use the following code in my CustomSlider component:
constructor(props) {
super(props);
this.handleWheel = this.handleWheel.bind(this);
}
componentDidMount() {
ReactDOM.findDOMNode(this).addEventListener('wheel', this.handleWheel);
}
componentWillUnmount() {
ReactDOM.findDOMNode(this).removeEventListener('wheel', this.handleWheel);
}
handleWheel(e) {
e.preventDefault();
e.deltaY > 0 ? this.slider.slickNext() : this.slider.slickPrev();
}
Component initialization
<Slider ref={slider => this.slider = slider}>
...
</Slider>
With hooks
const sliderRef = createRef();
const scroll = useCallback(
y => {
if (y > 0) {
return sliderRef?.current?.slickNext(); /// ? <- using description below
} else {
return sliderRef?.current?.slickPrev();
}
},
[sliderRef]
);
useEffect(() => {
window.addEventListener("wheel", e => {
scroll(e.deltaY);
});
}, [scroll]);
I used optional chaining from typescript connected with babel plugins, but you can use verification like: sliderRef.current && sliderRef.current.slickNext()
I was able to get scrolling to work in a function component that reference the Slider component (react-slick JS library) using hooks (useRef to obtain a reference to the Slider component and useEffect to add and remove a listener (scroll function) to the wheel event).
const myComponent () => {
const settings = {
dots: true,
slidesToShow: 1,
slidesToScroll: 1,};
const slider = useRef(null);
function scroll(e){
if (slider === null)
return 0;
e.wheelDelta > 0 ? (
slider.current.slickNext()
) : (
slider.current.slickPrev()
);
};
useEffect(() => {
window.addEventListener("wheel", scroll,true);
return () => {
window.removeEventListener("wheel", scroll, true);
};
}, []);
return (
<Slider {...settings} ref={slider}>
</Slider>
);
}
export default myComponent;