I need to pass "notecards" (an array) down from "Notecard.js" to "LoadQuestions.js". Console log shows that it is passing, but when I use {notecards} within the "return" it errors as "undefined". Could you please take a look?
Notecard.js (without the imports):
const useStyles = makeStyles((theme) => ({
root: {
maxWidth: 345,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
}));
export default function Notecard( {notecards} ) {
const classes = useStyles();
const next = () => {
console.log('Next Button Clicked')
};
const previous = () => {
console.log('Back Button Clicked')
};
const hint = () => {
console.log('Hint Button Clicked')
};
console.log({notecards});
return (
<Card className={classes.root}>
<div id="cardBody">
<CardHeader
title="Kate Trivia"
// subheader="Hint: In the 20th century"
/>
<CardContent>
<LoadQuestions notecards={notecards}/>
</CardContent>
</div>
</Card>
);
}
LoadQuestions.js (without imports)
const {useState} = React;
export default function LoadQuestions( {notecards} ) {
const [currentIndex, setCounter] = useState(0);
console.log({notecards});
return (
<div>
<Toggle
props={notecards}
render={({ on, toggle }) => (
<div onClick={toggle}>
{on ?
<h1>{props.notecards} hi</h1> :
<h1>{this.props[currentIndex].backSide}</h1>
}
</div>
)}
/>
<button onClick={() => {
console.log({notecards})
if (currentIndex < (this.props.length-1)) {
setCounter(currentIndex + 1);
} else {
alert('no more cards')
}
}}>Next Card
</button>
<button onClick={() => {
if (currentIndex > 0 ) {
setCounter(currentIndex -1);
} else {
alert('no previous cards')
}
}}>Previous Card
</button>
</div>
);
}
Thanks in advance!
That's all the details I have for you, but stack overflow really wants me to add more before it will submit. Sorry!
You should check if props exists, first time it renders the component it has no props so it shows undefined.
First i must say you destructured notecards out, so no need to use props.
If you want to use props you should change
({notecards}) to (props)
and if not you can directly use notecards since it is destructured
I suggest you two ways
adding question mark to check if exists
<h1>{props?.notecards} hi</h1>//in the case you want to use props
or
add the props in a if statement
<h1>{props.notecards?props.notecards:''} hi</h1> // if notecards is destructured remove the "props."
Related
I'm trying to display fields based on the value of a props so let's say my props value = 2 then I want to display 2 inputs but I can't manage to get it work.
This is what I tried
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
<div>
<p style={{color:'black'}}>Tier {tier + 1}</p>
<InputNumber />
</div>
})
}
const onPropsValueLoaded = (value) => {
let tmp = value
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(value).keys()
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}
useEffect(() => {
onPropsValueLoaded(props.numberOfTiers);
}, [])
return (
<>
<Button type="primary" onClick={showModal}>
Buy tickets
</Button>
<Modal
title="Buy ticket"
visible={visible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<p style={{ color: 'black' }}>{props.numberOfTiers}</p>
{loadFields.length ? (
<div>{addField()}</div>
) : null}
<p style={{ color: 'black' }}>Total price: </p>
</Modal>
</>
);
so here props.NumberOfTiers = 2 so I want 2 input fields to be displayed but right now none are displayed even though loadFields.length is not null
I am displaying this inside a modal (even though I don't think it changes anything).
I am doing this when I load the page that's why I am using the useEffect(), because if I use a field and update this onChange it works nicely.
EDIT:
I changed the onPropsValueLoaded() function
const generateArrays = Array.from({length : tmp}, (v,k) => k)
instead of
const generateArrays = Array.from(value).keys()
There are couple of things you should fix in here,
First, you need to return div in addField function to render the inputs.
Second, you should move your function onPropsValueLoaded inside useEffect or use useCallback to prevent effect change on each render.
Third, your method of creating array using Array.from is not correct syntax which should be Array.from(Array(number).keys()).
So the working code should be , I also made a sample here
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
return (
<div key={tier}>
<p style={{ color: "black" }}>Tier {tier + 1}</p>
<input type="text" />
</div>
);
});
};
useEffect(() => {
let tmp = 2; // tier number
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(Array(tmp).keys());
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}, [numberOfFields]);
return (
<>
<button type="button">Buy tickets</button>
<p style={{ color: "black" }}>2</p>
{loadFields.length ? <div>{addField()}</div> : null}
<p style={{ color: "black" }}>Total price: </p>
</>
);
}
Those are the three components I’m using, excluding the component that displays them in the DOM, but that’s not needed.
Here I have a Parent and two Child components.
For some reason when the popup is active, and I click the Refresh Child 1 Component button, it changes the state back to Child1, but I lose the functionality within that component. So the popUpToggle function stops working.
It was working fine before. When I click the Refresh Child 1 Component again however, it starts working. Why is that?
import React, { useState, useEffect } from 'react';
import Child1 from './child1'
import Child2 from './child2'
const Parent = () => {
const [display, setDisplay] = useState('');
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(!popUp)
console.log('PopUp Toggle ran')
};
const reRenderComponent= () => {
setRenderCount(renderCount + 1);
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
popUpToggle();
console.log('reRenderComponent ran, and the key is ' + renderCount)
};
useEffect(() => {
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
}, [])
return (
<div>
<button
style={{position: 'fixed', zIndex: '999', right: '0'}}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp ? <Child2 popUpToggle={popUpToggle}/> : null}
{display}
</div>
);
};
export default Parent;
Child 1:
import React from 'react';
const Child1 = ({ popUpToggle, renderCount }) => {
return (
<>
<button onClick={popUpToggle}>
Pop Up Toggle function
</button>
<h1>Child 1 is up, count is {renderCount}</h1>
</>
);
};
export default Child1;
Child 2:
import React, { useState } from 'react';
const Child2 = ({ popUpToggle }) => {
return (
<div
style={{
backgroundColor: 'rgba(0,0,0, .7)',
width: '100vw',
height: '100vh',
margin: '0',
}}
>
<h1>Child 2 is up</h1>
<h2>PopUp active</h2>
<button onClick={popUpToggle}>Toggle Pop Up</button>
</div>
);
};
export default Child2;
setDisplay(<Child1 /*etc*/ />);
Putting elements into state is usually not a good idea. It makes it very easy to cause bugs exactly like the one you're seeing. An element in state never gets updated, unless you explicitly do so, so it can easily refer to stale data. In your case, i think the issue is that the child component has a stale reference to popUpToggle, which in turn has an old instance of popUp in its closure.
The better approach, and the standard one, is for your state to contain just the minimal data. The elements get created when rendering, based on the data. That way, the elements are always in sync with the latest data.
In your case it looks like all the data already exists, so we don't need to add any new state variables:
const Parent = () => {
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(prev => !prev);
};
const reRenderComponent = () => {
setRenderCount(prev => prev + 1);
popUpToggle();
};
return (
<div>
<button
style={{ position: "fixed", zIndex: "999", right: "0" }}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp && <Child2 popUpToggle={popUpToggle} />}
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
</div>
);
};
I'm in the process of refactoring some of our components so I'm trying to incorporate memoization as some components may re-render with the same values (for example, hotlinked image URLs unless they are the same).
I have a simple component:
const CardHeader = props => {
// img is a stringand showAvatar is a boolean but it's always true
const { ..., showAvatar, img } = props;
return (
<CardHeader>
<ListItem>
// AvatarImage shouldn't re-render if img is the same as previous
{showAvatar && <AvatarImage img={img} />
</ListItem>
</CardHeader>
);
}
And then the AvatarImage:
const AvatarImage = React.memo(props => {
console.log("why is this still re-rendering when the img value hasn't changed?");
const { img } = props;
return (
<ListItemAvatar>
{img ?
<Avatar src={img} />
:
<Avatar>
Some initials
</Avatar>
}
</ListItemAvatar>
);
});
I have also tried passing in second argument of memo:
(prevProps, nextProps) => {
return true; // Don't re-render!
}
But the console.log still shows every time. I'm obviously missing something here or don't quite understand how this works. This component is a few levels down, but it passes in the img if it's available every time so I'd expect it to know that if the img was passed in the previous render and it's the same it knows not to re-render it again but for some reason it does?
Thanks all. It's much appreciated.
Well it is either showAvatar is not always true or CardHeader ListItem component magically decides whether show children or not
Example
const { useState, useEffect, memo, createContext, useContext } = React;
const getAvatars = () => Promise.resolve([
{
src: 'https://i.picsum.photos/id/614/50/50.jpg'
},
{
src: 'https://i.picsum.photos/id/613/50/50.jpg'
}
])
const Avatar = ({src}) => {
console.log('avatar render');
return <img src={src} alt="avatar"/>
}
const MemoAvatarToggle = memo(({src}) => {
console.log('memo avatar with \'expression &&\' render');
return <div>
{src ? <img src={src} alt="avatar"/> : <div>Test </div>}
</div>
})
const CardHeader = ({children}) => {
const luck = Boolean(Math.floor(Math.random() * 1.7));
return <div>
{luck && children}
</div>
}
const ListItem = ({children}) => {
return <div>
{children}
</div>
}
const ShowAvatarContext = createContext()
const App = (props) => {
const [avatars, setAvatars] = useState([]);
const [toggle, setToggle] = useState(false);
const [showAvatar, setShowAvatar] = useContext(ShowAvatarContext);
useEffect(() => {
let isUnmounted = false;
let handle = null;
setTimeout(() => {
if(isUnmounted) {
return;
}
setShowAvatar(true);
}, 500);
getAvatars()
.then(avatars => {
if(isUnmounted) {
return;
}
setAvatars(avatars)
})
const toggle = () => {
setToggle(prev => !prev);
handle = setTimeout(toggle, 1000);
//setShowAvatar(prev => !prev);
}
handle = setTimeout(toggle, 1000);
return () => {
isUnmounted = true;
clearTimeout(handle);
}
}, []);
return <div>
<CardHeader>
<ListItem>
{showAvatar && avatars.map((avatar, index) => <MemoAvatarToggle key={index} src={avatar.src}/>)}
</ListItem>
</CardHeader>
{toggle ? 1 : 0}
</div>
}
const ShowAvatarProvider = ({children}) => {
const state = useState(false);
return <ShowAvatarContext.Provider value={state}>
{children}
</ShowAvatarContext.Provider>
}
ReactDOM.render(
<ShowAvatarProvider>
<App/>
</ShowAvatarProvider>,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
Do you have StrictMode enabled? That will cause a component memoized with React.memo to render twice.
More information:
https://reactjs.org/docs/strict-mode.html
My React Component is rendering twice because of Strict Mode
memo will not block re-render if the component is actually referenced the changing props or functions.
In your scenario your AvatarImage referenced img, in this case if parent's state's img is changed, then your component will be re-rendered.
Alternatively, if your parent is just changed other props instead of img, then the AvatarImage will NOT be re-rendered.
Alternatively, if any props but you didn't add memo to AvatarImage, then AvatarImage will be re-rendered for each of parent's state updated.
You need to memorized img props too.
const CardHeader = props => {
const { showAvatar, img } = props;
const updatedIMG = React.useMemo(() => img, []);
return (
<CardHeader>
<ListItem>
{showAvatar && <AvatarImage img={updatedIMG} />
</ListItem>
</CardHeader>
);
}
Above one would work
I have 2 onClick functions
function VisitGallery(name) {
const history = useHistory();
console.log("visitgallery", name)
history.push("/gallery")
}
function App() {
const accesstoken = "******************"
const [viewport, setviewport] = React.useState({
latitude: ******
longitude: *******
width: "100vw",
height: "100vh",
zoom: 11
})
const [details, setdetails] = React.useState([
])
React.useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore()
const data = await db.collection("data").get()
setdetails(data.docs.map(doc => doc.data()))
}
fetchData();
}, [])
const [selectedpark, useselectedpark] = React.useState(null);
React.useEffect(() => {
const listener = e => {
if (e.key === "Escape") {
useselectedpark(null);
}
};
window.addEventListener("keydown", listener)
return () => {
window.removeEventListener("keydown", listener)
}
}, [])
return (
<div className="App">
<ReactMapGl {...viewport}
mapboxApiAccessToken={accesstoken}
mapStyle="mapbox://**************"
onViewportChange={viewport => {
setviewport(viewport)
}}>
{details.map((details) =>
<Marker key={details.name} latitude={details.lat} longitude={details.long}>
<button class="marker-btn" onClick={(e) => {
e.preventDefault();
useselectedpark(details);
}}>
<img src={icon} alt="icon" className="navbar-brand" />
</button>
</Marker>
)}
{selectedpark ?
(<Popup
latitude={selectedpark.lat}
longitude={selectedpark.long}
onClose={() => {
useselectedpark(null);
}}
>
<div>
<Card style={{ width: '18rem' }}>
<Card.Body>
<Card.Title>{selectedpark.name}</Card.Title>
<Card.Text>
{selectedpark.postalcode}
</Card.Text>
<Button variant="primary" onClick = VisitGallery() >Visit Gallery</Button>
</Card.Body>
</Card>
</div>
</Popup>)
: null}
{
console.log("in render", details)
}
</ReactMapGl>
</div>
);
}
export default App;
The outer onClick is assigned when the marker is first created, and when it is clicked the useselectedpark function is called, details is then assigned to selectedpark.
The inner onClick is assigned to the function VisitGallery(). When the inner onClick is triggered, i want to navigate to another page, hence the history.push().
Ideally, what i want for it to happen is, when the outer onClick is triggered, the cardview shows, and i have an option to visit the next page, which can be triggered by an onClick within the card. However, what is happening right now is both the onClicks are triggered when i click on the thumbnail. How do i fix it such that it is how i want it to be ideally?
ps: do let me know if my explanation is confusing and i will edit it accordingly
Try adding your second onClick into a callback function?
<Button variant="primary" onClick='()=>{ VisitGallery() }' >Visit Gallery</Button>
So that it doesn't automatically invoke the function until the click is triggered.
I have the following class that works fine (I know that DOMSubtreeModified is depreacted, I will update this too). It's a very basic WYIWYG I'm trying to refactor to a hook:
export class TextEditorClass extends React.Component {
constructor(props) {
super(props)
this.state ={
// this props contains the html content of the contentEditable
content: this.props.content,
}
}
componentDidMount() {
// here I add a listener to the contentEditable div that calls updateContent
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
rangy.init();
}
setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
updateContent() {
this.props.setContent('content', document.getElementById('editor').innerHTML);
this.setState({
content: document.getElementById('editor').innerHTML,
})
}
render() {
return (
<div className='editor-content dashed'>
<input id="myInput" type="file" ref={(ref) => this.upload = ref} style={{ display: 'none' }} />
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
}
This is what I wrote so far:
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
function updateContent() {
value.dispatch({type: 'content', value: document.getElementById('editor').innerHTML});
}
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
But it doesn't work:
Warning: A component is changing a controlled input of type text to be
uncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
Second warning:
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
The reason behind this I suppose is the fact that the typing triggers a loop of rerenders. Why it doesn't happen in the class?
EDIT: I believe the key to fix the issue is in useEffect(); I suppose it behaves differently from componentDidMount, in fact in the hook version I had to swap these two lines to prevent another error from occurring from this:
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
to this:
document.getElementById("editor").innerHTML = this.props.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
EDIT 2:
Adding [] as a parameter for useEffect() I got rid of the first error:
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
}, []);
More info about it here: https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
Your issue does not happen in React class component because componentDidMount is run once and you put the code of componentDidMount in useEffect without any conditions - which means it will run on each render.
Problems with your useEffect:
1) You are adding an event listender but you are not doing a clean-up afterwards.
2)You are mutating the DOM directly which is an anti pattern
3)You are applying any depedencies on your useEffect, which execut the useEffect on each render and will cause a memory leak.
Here is how your code should look like: (will be writing code shortly)
...
const [editorValue, setEditorValue] = React.useState('')
// If you want your hook to run once
React.useEffect( () => {
setEditorValue(value.state.content)
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
// The returning funnction will be executed on unmounting
() => document.getElementById("editor").removeEventListener("DOMSubtreeModified", () => updateContent(), false);
// note the `[]`] as second argument. That means to run only once.
}, [])
And the content of your #editor is then the controllable state editorValue.
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
{editorValue}
</div>
Matthew's answer pointed me to the right direction, and after a bit of tweaking I came up with this code that works fine:
import React from 'react'
import { Icon, IconButton, ButtonGroup, ButtonToolbar } from 'rsuite'
import rangy from 'rangy';
import 'rangy/lib/rangy-classapplier';
import dompurify from 'dompurify';
import './TextEditor.css';
import { ManagerContext } from './LessonManager.js';
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
// here I initialize the editor content with the content I receive from the provider
const [editorValue, setEditorValue] = React.useState(value.state.content);
React.useEffect( () => {
rangy.init();
const editor = document.getElementById('editor');
// here I replaced the deprecated DOMSubtreeModified with MutationObserver
// basically when you change the dom of the contentEditable I call dispatch
// and update the content
let mutationObserver = new MutationObserver(function(mutations) {
value.dispatch({type: 'content', value: editor.innerHTML});
});
mutationObserver.observe(editor, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
// the cleanup can be done with .disconnect();
return () => mutationObserver.disconnect();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
// the createMarkup() function is used to generate the markup that I will put inside
// my contentEditable, I installed an additional npm package (dompurify) to clean up
// the markup to prevent XSS attacks
// https://dev.to/jam3/how-to-prevent-xss-attacks-when-using-dangerouslysetinnerhtml-in-react-1464)
function createMarkup() {
const sanitizer = dompurify.sanitize;
return {__html: sanitizer(editorValue)}
};
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
// here I insert the html with the purified html
dangerouslySetInnerHTML={createMarkup()}
>
</div>
</div>
)
}
To give a complete answer I will add also the parent components. This is the LessonContents component:
import React, { useEffect } from 'react';
import { Input } from 'rsuite';
// TextEditorHook is the WYSIWYG component
import { TextEditorHook } from './TextEditor.js';
// this context contain the value of the lesson manager
// we will use the context for the lesson's title, desc, content and location
import { ManagerContext } from './LessonManager.js';
// truncate is a function to shorten text in order to make it fit
// maxLenght is the default max text length
import { truncate, maxLength } from '../Common.js';
export default function Menu() {
const value = React.useContext(ManagerContext);
useEffect(() => {
// if not editing inject the html in the display div on load
if (!value.editing) document.querySelector('.lesson-content').innerHTML = value.state.content;
console.log(value);
});
const editorContent = () => {
return (
<React.Fragment>
<Input
className='rsuite-input dashed'
placeholder='Lesson title'
value={value.state.title}
onChange={(v) => value.dispatch({type: 'title', value: v})}
/>
<Input
className='rsuite-input dashed'
componentClass="textarea"
rows={1}
style={{ width: '100%' }}
placeholder='Lesson description'
value={value.state.desc}
onChange={(v) => value.dispatch({type: 'desc', value: v})}
/>
<TextEditorHook/>
</React.Fragment>
)
}
const viewerContent = () => {
return (
<React.Fragment>
<div className='content'>
<span>{truncate(value.state.title, maxLength, false)}</span>
</div>
<div className='content'>
<span>{value.state.desc}</span>
</div>
<div className='content'>
<span>{truncate(value.state.location, maxLength, false)}</span>
</div>
<div className='content justify-left lesson-content'>
</div>
</React.Fragment>
)
}
return (
<React.Fragment>
{value.editing ? editorContent() : viewerContent()}
</React.Fragment>
)
}
And finally the LessonManager component:
import React from 'react';
import LessonMenu from './LessonMenu.js';
// this context will be used from child components to access the lesson status
import LessonContents from './LessonContents.js';
export const ManagerContext = React.createContext(null);
const initialState = {
title: 'Lesson title',
desc: 'Lesson description',
content: 'Lesson content',
location: 'home / english'
};
function reducer(state, action) {
console.log(action.type, action.value, state)
switch (action.type) {
case 'title':
return {...state, title: action.value};
case 'desc':
return {...state, desc: action.value};
case 'content':
return {...state, content: action.value};
default:
throw new Error();
}
}
export default function LessonManager() {
const [editing, toggleEditor] = React.useState(true);
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = React.useMemo(() => {
return {
state,
dispatch,
editing,
toggleEditor,
}
}, [state, editing]);
return (
<ManagerContext.Provider value={value}>
<div className='box-default expand'>
<div className='handle' style={{display: 'flex', justifyContent: 'center', width: '100%', cursor: 'grab'}}>
<LessonMenu />
</div>
<LessonContents />
</div>
</ManagerContext.Provider>
)
}
I am not completely sure this is the best approach to solve this problem, but it works fine.