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>
);
}
}
Related
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]}
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]);
I'm using ant calendar which renders PanelBody(third party) component which has a prop call rowNum. Now I want to override this value to my custom value from my component:
import React from 'react';
import { Calendar } from 'antd';
import moment from 'moment';
import { Button } from '#material-ui/core';
import { getEventInfo, getQuest } from '../../../response/api';
import { EventInfo, Event } from '../../../response/type';
import { ConfigProvider } from 'antd';
import jaJP from 'antd/lib/locale/ja_JP';
interface State {
event_infos: EventInfo[];
events: Event[];
}
interface Props {
}
class EventCalendar extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
event_infos: [],
events: []
};
}
componentDidMount() {
getEventInfo().then((item) => {
this.setState({event_infos: item});
});
getQuest().then((item) => {
this.setState({events: item});
});
}
getCorrectFormatDate = date => {
return moment(date, 'YYYY-MM-DD').format('YYYY-MM-DD');
}
getListEventInfoDate = value => {
let listData = [];
let listEventInfo = this.state.event_infos;
listEventInfo.map((item) => {
const startTime = this.getCorrectFormatDate(item.start_at);
const calendarTime = this.getCorrectFormatDate(value);
if (startTime === calendarTime) {
listData.push(item);
}
return listData;
});
return listData || [];
}
getListEventDate = value => {
let listData = [];
let listEvent = this.state.events;
listEvent.map((item) => {
const startTime = this.getCorrectFormatDate(item.start_at);
const calendarTime = this.getCorrectFormatDate(value);
if (startTime === calendarTime) {
listData.push(item);
}
return listData;
});
return listData || [];
}
dateCellRender = value =>{
const listEventInfos = this.getListEventInfoDate(value);
const listEvents = this.getListEventDate(value)
return (
<ul className="events-calendar">
{listEventInfos.map((item, i) => <img key={i} src={item.image} alt="event_info" />)}
{listEvents.map((item, i) => <Button key={i} variant="outlined" color="primary" className="button-event">{item.name}</Button>)}
</ul>
)
}
render() {
moment.locale("ja");
return(
<ConfigProvider locale={jaJP}>
<Calendar
dateCellRender={this.dateCellRender}
className="hide-header"
validRange={[moment('2020-12-13', 'YYYY-MM-DD'), moment('2021-01-16', 'YYYY-MM-DD')]}
defaultValue={moment('2020-12-13')}
/>
</ConfigProvider>
)
}
};
export default EventCalendar;
This component render Calendar which renders PanelBody. I tried to create a duplicated component then overrided the prop value, imported it to EventCalendar, but it cannot update the props value as expected. Is it my approach wrong? Any suggestion for this issue?
Disclaimer: I've seen Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null but no solution from this question fixes my problem
I am trying to render a component inside a React route like this in App.js
<main style={{marginTop: '1px'}}>
<div className="App">
<Switch>
<Route exact path ='/' render = {() => <Home toggleFilter={this.state.filterDrawerOpen}/>}/>
<Route path='/profile' component ={Profile}/>
</Switch>
</div>
</main>
But the Home component never get's rendered even tho I can see console logs from the Home component render in the console like in this picture console logs
The home component is 400+ lines long so I'll include just the relevant code
import React, { Component } from 'react';
import Auth from '#aws-amplify/auth';
import { API } from 'aws-amplify';
import ProfileRedirect from "./components/ProfileRedirect";
import LoadingAnimation from './components/LoadingAnimation';
import ReadingSpeed from "./components/ReadingSpeed";
import './Tags.css';
import Articles from "./components/Articles";
import { CSSTransitionGroup } from 'react-transition-group'
import FilterArea from './components/FilterArea';
import "react-datepicker/dist/react-datepicker.css";
import FilterDrop from './components/FilterDrop';
import FilterDrawer from './components/FilterDrawer';
import { withRouter } from "react-router";
let apiName = 'userApi';
let path = '/users/';
class Home extends Component {
constructor(props){
super(props);
this.state = {
isLoading: true,
firstLogIn: true,
filterDrawerOpen:false,
user :{
phone:"",
readingSpeed:0,
email:"",
username:"",
articles: [],
interests: [],
saved:[],
filters:{
minutes:null,
authors:[],
sources:[],
minDate:{},
selectedInterests:[],
selectedDate:{}
}
}
}
this.dateFilter = this.dateFilter.bind(this)
}
async componentDidMount(){
let userEntry;
let date = new Date();
date.setMonth(date.getMonth()-1)
// const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const loggedUser = await Auth.currentAuthenticatedUser();
userEntry = await API.get(apiName,path + loggedUser.username);
if(userEntry.hasOwnProperty("userName")){
let uniqueResults;
let results = await this.callDatabase(userEntry)
uniqueResults = results.reduce(function (p, c) {
if (!p.some(function (el) {return (el.title === c.title && el.author === c.author);}))
p.push(c);
return p;
}, []);
this.setState({
isLoading:false,
firstLogIn:false,
filterDrawerOpen:false,
user : {
phone:userEntry.userName,
readingSpeed:userEntry.readingSpeed,
email:userEntry.userEmail,
username:userEntry.userName,
articles: uniqueResults,
interests:userEntry.userInterests,
saved: userEntry.savedArticles,
filters:{
minutes:null,
authors:[],
sources:[],
minDate:date,
selectedDate:{},
selectedInterests:[]
}
}
})
}else {
this.setState({
isLoading:false
})
}
}
async callDatabase (userEntry,sources,freeMode){...}
authorFilter = selected => {...}
sourceFilter = selected => {...}
interestFilter = selected => {...}
minutesFilter(value) {...}
componentWillReceiveProps(newProps) {
if(newProps.toggleFilter !== this.props.toggleFilter){
this.filterToggleClickHandler();
}
}
filterToggleClickHandler = () => {...}
filterDropClickHandler = () => {...}
dateFilter(selected) {...}
generateOptions = filter => {
let data;
if (filter === "author"){
data = this.state.user.articles.reduce(function (p, c) {
if (!p.some(function (el) { return (el.author === c.author); }))
p.push(c);
return p;
}, [])
}else if (filter==="source"){
let tempData;
tempData = this.state.user.articles.reduce(function (p, c) {
if (!p.some(function (el) { return (el.source.name === c.source.name); }))
p.push(c);
return p;
}, [])
data = tempData.map(element => element.source)
}else if (filter==="interest"){
data = this.state.user.articles.reduce(function (p, c) {
if (!p.some(function (el) { return (el.interest === c.interest); }))
p.push(c);
return p;
}, [])
}
return data
}
async updateDataBase(readingSpeed){
let updates = {
body:{
userName:this.state.user.username,
userEmail:this.state.user.email,
userPhone:this.state.user.phone,
userInterests:this.state.user.interests,
savedArticles:this.state.user.saved,
readingSpeed:readingSpeed,
}
}
return await API.put(apiName,path,updates);
}
filtersArea() {
let check
let newDate = this.state.user.filters.selectedDate;
if(newDate === null){
check = true
}else {
check= Object.entries(newDate).length === 0 && newDate.constructor === Object
}
return (
<div className="container-fluid">
<div className="col" style={{margin:"0",padding:"6"}}>
<FilterArea
sourceOptions = {this.generateOptions("source")}
interestOptions = {this.generateOptions("interest")}
authorOptions = {this.generateOptions("author")}
sourceFilter = {this.sourceFilter.bind(this)}
interestFilter = {this.interestFilter.bind(this)}
authorFilter = {this.authorFilter.bind(this)}
selected={!check ? this.state.user.filters.selectedDate:undefined}
minDate = {this.state.user.filters.minDate}
dateFilter = {this.dateFilter.bind(this)}
minutesFilter = {this.minutesFilter.bind(this)}
/>
<FilterDrawer
show = {this.state.filterDrawerOpen}
sourceOptions = {this.generateOptions("source")}
interestOptions = {this.generateOptions("interest")}
authorOptions = {this.generateOptions("author")}
sourceFilter = {this.sourceFilter.bind(this)}
interestFilter = {this.interestFilter.bind(this)}
authorFilter = {this.authorFilter.bind(this)}
selected={!check ? this.state.user.filters.selectedDate:undefined}
minDate = {this.state.user.filters.minDate}
dateFilter = {this.dateFilter.bind(this)}
minutesFilter = {this.minutesFilter.bind(this)}
/>
</div>
</div>
);
}
checkAuthors(filter,data){
let result = [];
let articles = data.map(function(article){
if(filter.includes(article.author))result.push(article);
})
return result
}
checkSource(filter,data){
let result = [];
let articles = data.map(function(article) {
if(filter.includes(article.source.name)) result.push(article)
})
return result
}
checkInterest(filter,data){
let result = [];
let articles = data.map(function(article){
if(filter.includes(article.interest))result.push(article);
})
return result
}
checkMinutes(filter,filter1,data){
let result = [];
let articles = data.map(function (article) {
if(article.hasOwnProperty("charNumber")){
if((article.charNumber/filter1)<=filter)result.push(article)
}
})
return result
}
checkDate(filter,data){
let result = [];
let dA;
let dB = filter;
let articles = data.map(function(article){
dA = new Date(article.publishedAt.substring(0,10))
if(dB<=dA) result.push(article)
})
return result
}
render() {
let filterdrop;
if(this.state.filterDrawerOpen) {
filterdrop = <FilterDrop click = {this.filterDropClickHandler}/>
}
console.log(this.state)
const stillLoading = () => {
return (
<div className="loading">
<LoadingAnimation
type = {"spinningBubbles"}
color = {"aqua"}
/>
</div>);
}
const articles = (filterA, filterS, filterI, filterM) => {
let articles = this.state.user.articles;
let newDate = this.state.user.filters.selectedDate;
let readingTime = this.state.user.readingSpeed;
let check;
if(newDate === null){
check = true
}else {
check= Object.entries(newDate).length === 0 && newDate.constructor === Object
}
if(!check){
articles = this.checkDate(newDate,articles)
}
if(filterA.length){
articles = this.checkAuthors(filterA,articles)
}
if(filterS.length){
articles = this.checkSource(filterS,articles)
}
if(filterI.length){
articles = this.checkInterest(filterI,articles)
}
if(!(filterM === null) && filterM!==0){
articles = this.checkMinutes(filterM,readingTime,articles)
}
return(
<div className="wrapper">
{
articles.map(function(article) {
return (
<Articles
article = {article}
key = {article.id}
/>
)
})}
</div> );
}
if(this.state.isLoading){
return (
stillLoading()
);
}else if(!this.state.firstLogIn && this.state.user.articles.length>0 && this.state.user.readingSpeed >0){
return (
<CSSTransitionGroup
transitionName="example"
transitionAppear={true}
transitionAppearTimeout={1000}
transitionEnter={false}
transitionLeave={false}>
{this.filtersArea()}
{filterdrop}
{articles(this.state.user.filters.authors,this.state.user.filters.sources,
this.state.user.filters.selectedInterests,this.state.user.filters.minutes)}
</CSSTransitionGroup>
);
}else if(this.state.firstLogIn || this.state.user.readingSpeed === 0){
return(
<ReadingSpeed updateDb = {this.updateDataBase.bind(this)}/>
);
}
else if(this.state.user.interests.length === 0){
return(
<div>
<ProfileRedirect/>
</div>
);
}
}
}
export default Home;
I've tried things like
const MyHome =(props) => {
return (
<Home {...props} toggleFilter = {this.state.filterDrawerOpen}/>
)
}
Instead of rendering directly in the Route but nothing seems to work.
Here is the code for index.js as well
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.css';
import {BrowserRouter as Router, Route} from 'react-router-dom';
ReactDOM.render(
<Router>
<Route path='/' render={(props )=> <App {...props}/>}/>
</Router>,
document.getElementById('root'));
serviceWorker.unregister();
What am I missing?
I've been trying to dispatch an action in componentDidMount, unfortunately I'm getting:
Maximum call stack size exceeded
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import moment from 'moment';
import styles from './style.css';
import Arrow from './components/Arrow';
import RadioCheck from 'components/RadioCheck';
import Select from 'components/Select';
import DatePickerWrapper from 'components/DatePickerWrapper';
import { months } from 'utils';
import { changeMonth, changeFromDate, changeToDate, changeRadioDatepicker } from 'components/PostsPage/actions';
class DatePickerDropdown extends Component {
constructor(props) {
super(props);
this.firstChild = null;
this.state = {
open: false,
loadedPreferences: false,
};
this._handleSelectedClick = this._handleSelectedClick.bind(this);
this._handleRadioChange = this._handleRadioChange.bind(this);
this._handleFromDatepickerChange = this._handleFromDatepickerChange.bind(this);
this._handleToDatepickerChange = this._handleToDatepickerChange.bind(this);
this._handleMonthChange = this._handleMonthChange.bind(this);
this._handleOutsideClick = this._handleOutsideClick.bind(this);
}
componentDidMount() {
window.addEventListener('click', this._handleOutsideClick, false);
setTimeout(() => {
this.loadPreferences();
},5)
}
componentWillUnmount() {
window.removeEventListener('click', this._handleOutsideClick, false);
}
loadPreferences() {
const monthId = preferences.get(preferences.keys.DATEPICKER_MONTH);
const dateFrom = preferences.get(preferences.keys.DATEPICKER_FROM);
const dateTo = preferences.get(preferences.keys.DATEPICKER_TO);
const filterType = preferences.get(preferences.keys.DATEPICKER_FILTER_TYPE);
if (monthId !== null) {
this.props.changeMonth(monthId);
}
if (dateFrom !== null) {
this.props.changeFromDate(moment(dateFrom));
}
if (dateTo !== null) {
this.props.changeToDate(moment(dateTo));
}
if (filterType !== null) {
this.props.changeRadio(filterType);
}
}
getRange() {
const { datepickerFilter, month, dateFrom, dateTo} = this.props;
if (datepickerFilter === 'month') {
return {
start: moment().month(month).startOf('month').format('D. MMMM YYYY'),
end: moment().month(month).endOf('month').format('D. MMMM YYYY')
}
}
if (datepickerFilter === 'from_to') {
return {
start: dateFrom.format('D. MMMM YYYY'),
end: dateTo.format('D. MMMM YYYY'),
};
}
}
toggleSelect(show = null) {
if (show !== null) {
this.setState(() => ({open: show}));
}
if (show === null) {
this.setState(() => ({open: !this.state.open}));
}
}
_handleSelectedClick() {
this.toggleSelect();
}
_handleOutsideClick(e) {
if (!ReactDOM.findDOMNode(this).contains(e.target) && this.state.open) {
this.toggleSelect(false);
}
}
_handleFromDatepickerChange(date) {
if (this.props.dateFrom.toDate() !== date.toDate()) {
this.props.changeFromDate(date);
preferences.store(preferences.keys.DATEPICKER_FROM, date.toDate());
}
}
_handleToDatepickerChange(date) {
if (this.props.dateTo.toDate() !== date.toDate()) {
this.props.changeToDate(date);
preferences.store(preferences.keys.DATEPICKER_TO, date.toDate());
}
}
_handleMonthChange(month) {
if (this.props.month !== month) {
this.props.changeMonth(month);
preferences.store(preferences.keys.DATEPICKER_MONTH, month);
}
}
_handleRadioChange(filterType) {
if (this.props.datepickerFilter !== filterType) {
this.props.changeRadio(filterType);
preferences.store(preferences.keys.DATEPICKER_FILTER_TYPE, filterType);
}
}
render() {
const dropdownClass = this.state.open ? styles.dropdownActive : styles.dropdown;
const dropdownButtonClass = this.state.open ? styles.selectedActive : styles.selected;
const arrowClass = this.state.open ? styles.arrowActive : styles.arrow;
const range = this.getRange();
return (
<div className={styles.container}>
<div className={dropdownButtonClass} onClick={this._handleSelectedClick}>
<div className={styles.date}>{range.start}<span>to</span>{range.end}</div>
<div className={arrowClass}>
<Arrow up={this.state.open} size={10} invert={this.props.invert}/>
</div>
</div>
<div className={dropdownClass}>
<div className={styles.datepickerRow}>
<div>
<RadioCheck label={'Filter by Month'} type="radio" id="month" name="datepicker_radio" value="month" checked={this.props.datepickerFilter === 'month'} onChange={this._handleRadioChange}/>
</div>
<div className={styles.datepickerRowInner}>
<span>Month</span>
<div className={styles.inputItem}>
<Select
options={months}
onChange={this._handleMonthChange}
optionsToShow={12}
small
defaultOption={this.props.month.toString()}
/>
</div>
</div>
</div>
<div className={styles.datepickerRow}>
<div>
<RadioCheck label={'Filter by Date range'} type="radio" id="from" name="datepicker_radio" value="from_to" onChange={this._handleRadioChange} checked={this.props.datepickerFilter === 'from_to'}/>
</div>
<div className={styles.datepickerRowInner}>
<span>from</span>
<div className={styles.inputItem}>
<DatePickerWrapper date={this.props.dateFrom} onChange={this._handleFromDatepickerChange}/>
</div>
</div>
</div>
<div className={styles.datepickerRow}>
<div className={styles.datepickerRowInner}>
<span>to</span>
<div className={styles.inputItem}>
<DatePickerWrapper date={this.props.dateTo} onChange={this._handleToDatepickerChange}/>
</div>
</div>
</div>
</div>
</div>
);
}
}
DatePickerDropdown.propTypes = {};
DatePickerDropdown.defaultProps = {};
const mapStateToProps = state => {
const { monthSelect, dateFrom, dateTo, datepickerFilter } = state.postsFilters;
return {
month: monthSelect,
dateFrom,
dateTo,
datepickerFilter
}
};
const mapDispatchToProps = dispatch => {
return {
changeMonth: (monthId) => dispatch(changeMonth(monthId)),
changeFromDate: (date) => dispatch(changeFromDate(date)),
changeToDate: (date) => dispatch(changeToDate(date)),
changeRadio: (val) => dispatch(changeRadioDatepicker(val)),
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(DatePickerDropdown);
What I'm trying to achieve is to load preferences from localStorage. If i wrap the call of this.loadPreferences() in a setTimeout() with a delay of 100ms it does work, though that doesn't feel right.
I guess the issue stems from the fact that I'm updating the same props that I'm mapping to that component. What would be a better approach to achieve my goal?
EDIT: added whole source to avoid confusion
Try to explicitly bind click hanlder to context this._handleOutsideClick.bind(this)