I'm trying to convert a dynamic tag naming system from React into a more hook like approach, what I'm doing is first I import and export a few components so that I can pull them all at once on a array, like this:
import Component1 from './Component1/Component1'
import Component2 from './Component2/Component2'
export {
Component1,
Component2
}
And then I load them like so:
import { useState, useEffect } from "react";
import * as Components from './../AllComponents/Components'
function importHook() {
const [states, setStates] = useState({
components: [],
currentComponent: 0
})
useEffect(() => {
Object.entries(Components).forEach(component => {
setStates(prevState => ({ components: [...prevState.components, component[1]]}))
})
}, []) // Look's into Components and adds every component to the 'components' array so that I can use that array in the next step.
return states;
}
export default importHook
And after that I proceed to do so:
import React from 'react'
import './MainComponent.scss'
import importHook from '../../importHook'
function MainComponent() {
const { components, currentComponent } = importHook() // Pull and set the values using the hook
const TagName = components[currentComponent] // Created the tag name by targeting the first function, seeing as currentComponent should be 0 at the time
console.log('Tag: ' + TagName) // undefined
return (
<div className="h-100">
<TagName /> // crashes everything
</div>
)
}
export default MainComponent
So I have figured out that the reason that <TagName /> crashes everything is because something runs twice.
If you remove <TagName /> so that you can get the output from console.log and change currentComponent inside the const TagName to 0, you notice that the first time console.log runs is returns undefined, while the second run returns the actual function that is stored inside the array.
So really my question is just, why / what executes twice? I have an idea, I'm assuming its because of the forEach that's adding to the array or something like that, but I'm not completely sure.
What can be done so that we can have all the values ready before returning here? I haven't tried this yet, but I assume I could introduce a if statement that would check and see if the variable is empty and if so, display some kind of loading screen or fidget, but this doesn't seem like the best solution to me, I know there are a lot of ways to do something but not all are good and since I'm really new to all of this its better to read / ask.
About the read part, couldn't find much about solving this or the implementation that I've mentioned, I have tried useEffect, useCallback and other such, not sure if I've been doing it wrong though...
Related
In my main function App(), in useEffect(), I get data (variable named document) using some API with success. My data is the Object with some fields ie title and text.
I pass this data through my functions and I want different functions to use different parts of the document. Later, I render title with success:
import React from 'react'
import "./Title.css"
function Title({document}) {
return (
<div className="title">
{document.title}
</div>
)
}
export default Title;
I want to render 'text' field too, BUT I'd like to split it on newlines
import {useState, useEffect} from 'react'
import React from 'react'
function SourceText({ document }) {
return (
<div>
{document.text.split()}
</div>
)
}
export default SourceText
Which does not work and returns error
TypeError: document.text is undefined
which is weird because when I return just document.text in divs, this object exists.
I tried to add something like this
const [text, setText] = React.useState(null);
React.useEffect(() => {
setText(document.text)
console.log(document.text);
}, []);
But it does not look like it sets document.text value to text variable. What I can't get too is that console in the browser prints the entire object after adding these lines of code.
I am new to React and JavaScript and I really don't get how to modify and use the value passed to a function.
Thanks
EDIT:
I pass the document from App to TabGroup, then to Source and then to SourceText. The last one is here:
import './Source.css';
import SourceText from './SourceText';
import Title from './Title';
function Source({document}) {
return (
<div className="source">
<Title title={document.title}/>
<SourceText document={document} />
</div>
);
}
export default Source;
As I mentioned Title always renders, and using just {document.text} in SourceText does work too
document.text.split() will return an array. str.split() without any parameters returns an array.
You cannot render arrays directly unless you loop through each item. Also, always add checks for null | undefined when trying to render values.
{document?.text && document.text.split().map...}
After #Tom Bombadil post I was able to find the solution
import {useState, useEffect} from 'react'
import React from 'react'
function SourceText({ document }) {
return (
<div>
{document.text && document.text.split('at').map((line) => (line))}
</div>
)
}
export default SourceText
EDIT: didn't take too long to realize my earlier notes were wrong. Seems like it was indeed some async issue - hard to understand for me because the error and result depend on the duration of data fetching.
Can't find documentation about this anywhere. Will this cause the useEffect to EVER run again? I don't want it to fetch twice, that would cause some issues in my code.
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
const myComponent = () => {
const { push } = useHistory();
useEffect( () => {
console.log(" THIS SHOULD RUN ONLY ONCE ");
fetch(/*something*/)
.then( () => push('/login') );
}, [push]);
return <p> Hello, World! </p>
}
From testing, it doesn't ever run twice. Is there a case that it would?
For the sake of the question, assume that the component's parent is rerendering often, and so this component is as well. The push function doesn't seem to change between renders - will it ever?
Ciao, the way you write useEffect is absolutely right. And useEffect will be not triggered an infinite number of time. As you said, push function doesn't change between renders.
So you correctly added push on useEffect deps list in order to be called after fetch request. I can't see any error in your code.
It's my first time working with React and I'm having some trouble with starting to use Axios. I watched a video, a very simple practical tutorial that showed how to use a get function, but I think something went wrong because even following the same exact steps I still get the error "this.state.persons.map is not a function". I want to stress the fact that the author of the video uses this exact same JavaScript code, and for him it works. Any explanation?
Here's the whole code for reference:
import React from "react";
import axios from "axios";
export default class personList extends React.Component{
state = {
persons: [],
};
componentDidMount(){
axios.get(`https://jsonplaceholder.typicode.com`)
.then(res =>{
console.log(res);
this.setState({persons: res.data});
});
}
render() {
return (
<ul>
{this.state.persons.map(person => <li key={person.id}>{person.name}</li>)}
</ul>
)
}
}
I looked around for an answer, but every other case that has been presented is either too different (using set arrays, json and whatnot) or it refers to a string used instead of an array, which causes the error, and obviously it's not my case.
You are making a GET request at https://jsonplaceholder.typicode.com which returns the whole webpage. If you want to fetch the users, use this URL instead: https://jsonplaceholder.typicode.com/users
I have a loading image in my page and a number of react components. In several components I need to implement the functionality of show/hide loading image.
So there are three options I can think of:
In each component, use a state variable and a loadingImage component to show hide the image. Like below code:
{this.state.showLoaidngImage ? <LoadingImage/> : null}
I can choose only to have this component at top-level component and let sub-components to call the parent display loading image method.
I can also use pure jquery here in each component and directly use the id to show/hide
The first approach seem to duplicate the component tags in each component and I am thinking of whether it is a good approach or not.
The second one is a bit complicated to implement.
The third approach seems dirty to me.
So which one should I use in the react world?
You should be going with the second approach because in that case you will not have to rewrite your loadingImage component again and according to the react good practices we should create components for everything, and use them wherever possible.
I think I would favor having your LoadingImage inside a component itself that handles hiding and showing via a prop. Something like this:
import React from 'react';
import PropTypes from 'prop-types';
import LoadingImage from 'where/this/exists';
const Loader = ({
show
}) => {
return (
{ show && <LoadingImage /> }
);
};
Loader.propTypes = {
show : PropTypes.bool.isRequired
};
export default Loader;
Then in your template:
<Loader show={ this.state.showLoader } />
(this result might be combined with #awildeep's response)
Assuming that you have React components that fetches to APIs, and those componends needs a "Loading image" separately, the first thing that comes into my mind is using redux and redux-promise-middleware.
Why?
You could have a global state, say for example
API_1: {
isFullfilled: false,
isRejected: false,
...
},
API_2: {
isFullfilled: false,
isRejected: false,
...
}
So, for instance, let's say that you have two React components that connects with those APIs. You will have two states!
{!this.state.API_1.isFullfilled && !this.state.API_1.isRejected : <LoadingImage /> : null }
Yes, this is too much code, but there's a way to simplyfy it, in mapStateToProps:
const mapStateToProps = state => {
const { API_1, API_2 } = state
return {
isFullfilled_API_1: API_1.isFullfilled,
isRejected_API_1: API_1.isRejected,
isFullfilled_API_2: API_2.isFullfilled,
isRejected_API_2: API_2.isRejected,
}
}
// Let's get those variables!
const { isFullfilled_API_1, isRejected_API_1 } = this.props
{ !isFullfilled_API_1 && !isRejected_API_1 ? <LoadingPage> : null}
You can track status for each component without a headache
You will accomplish your goal!
Hope it helps, and let me know if you have any concern!
MY QUESTION: Why doesn't updating a property of an object in an array in my Immutable state (Map) not cause Redux to update my component?
I'm trying to create a widget that uploads files to my server, and my initial state (from inside my UploaderReducer which you will see below) object looks like this:
let initState = Map({
files: List(),
displayMode: 'grid',
currentRequests: List()
});
I have a thunk method that starts uploads and dispatches actions when an event occurs (such as a progress update). For example, the onProgress event looks like this:
onProgress: (data) => {
dispatch(fileUploadProgressUpdated({
index,
progress: data.percentage
}));
}
I'm using redux-actions to create and handle my actions, so my reducer for that action looks like this:
export default UploaderReducer = handleActions({
// Other actions...
FILE_UPLOAD_PROGRESS_UPDATED: (state, { payload }) => (
updateFilePropsAtIndex(
state,
payload.index,
{
status: FILE_UPLOAD_PROGRESS_UPDATED,
progress: payload.progress
}
)
)
}, initState);
And updateFilePropsAtIndex looks like:
export function updateFilePropsAtIndex (state, index, fileProps) {
return state.updateIn(['files', index], file => {
try {
for (let prop in fileProps) {
if (fileProps.hasOwnProperty(prop)) {
if (Map.isMap(file)) {
file = file.set(prop, fileProps[prop]);
} else {
file[prop] = fileProps[prop];
}
}
}
} catch (e) {
console.error(e);
return file;
}
return file;
});
}
So far, this all seems to work fine! In Redux DevTools, it shows up as an action as expected. However, none of my components update! Adding new items to the files array re-renders my UI with the new files added, so Redux certainly doesn't have a problem with me doing that...
My top level component that connects to the store using connect looks like this:
const mapStateToProps = function (state) {
let uploadReducer = state.get('UploaderReducer');
let props = {
files: uploadReducer.get('files'),
displayMode: uploadReducer.get('displayMode'),
uploadsInProgress: uploadReducer.get('currentRequests').size > 0
};
return props;
};
class UploaderContainer extends Component {
constructor (props, context) {
super(props, context);
// Constructor things!
}
// Some events n stuff...
render(){
return (
<div>
<UploadWidget
//other props
files={this.props.files} />
</div>
);
}
}
export default connect(mapStateToProps, uploadActions)(UploaderContainer);
uploadActions is an object with actions created using redux-actions.
A file object in the files array is basically this:
{
name: '',
progress: 0,
status
}
The UploadWidget is basically a drag n drop div and a the files array printed out on the screen.
I tried using redux-immutablejs to help out as I've seen in many posts on GitHub, but I have no idea if it helps... This is my root reducer:
import { combineReducers } from 'redux-immutablejs';
import { routeReducer as router } from 'redux-simple-router';
import UploaderReducer from './modules/UploaderReducer';
export default combineReducers({
UploaderReducer,
router
});
My app entry point looks like this:
const store = configureStore(Map({}));
syncReduxAndRouter(history, store, (state) => {
return state.get('router');
});
// Render the React application to the DOM
ReactDOM.render(
<Root history={history} routes={routes} store={store}/>,
document.getElementById('root')
);
Lastly, my <Root/> component looks like this:
import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
export default class Root extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
routes: PropTypes.element.isRequired,
store: PropTypes.object.isRequired
};
get content () {
return (
<Router history={this.props.history}>
{this.props.routes}
</Router>
);
}
//Prep devTools, etc...
render () {
return (
<Provider store={this.props.store}>
<div style={{ height: '100%' }}>
{this.content}
{this.devTools}
</div>
</Provider>
);
}
}
So, ultimately, if I try to update a 'progress' in the following state object, React/Redux does not update my components:
{
UploaderReducer: {
files: [{progress: 0}]
}
}
Why is this? I thought the whole idea of using Immutable.js was that it was easier to compare modified objects regardless of how deeply you update them?
It seems generally getting Immutable to work with Redux is not as simple as it seems:
How to use Immutable.js with redux?
https://github.com/reactjs/redux/issues/548
However, the touted benefits of using Immutable seem to be worth this battle and I'd LOVE to figure out what I'm doing wrong!
UPDATE April 10 2016
The selected answer told me what I was doing wrong and for the sake of completeness, my updateFilePropsAtIndex function now contains simply this:
return state.updateIn(['files', index], file =>
Object.assign({}, file, fileProps)
);
This works perfectly well! :)
Two general thoughts first:
Immutable.js is potentially useful, yes, but you can accomplish the same immutable handling of data without using it. There's a number of libraries out there that can help make immutable data updates easier to read, but still operate on plain objects and arrays. I have many of them listed on the Immutable Data page in my Redux-related libraries repo.
If a React component does not appear to be updating, it's almost always because a reducer is actually mutating data. The Redux FAQ has an answer on that topic, at http://redux.js.org/docs/FAQ.html#react-not-rerendering.
Now, given that you are using Immutable.js, I'll admit that mutation of data seems a bit unlikely. That said... the file[prop] = fileProps[prop] line in your reducer does seem awfully curious. What exactly are you expecting to be going on there? I'd take a good look at that part.
Actually, now that I look at it... I am almost 100% certain that you are mutating data. Your updater callback to state.updateIn(['files', index]) is returning the exact same file object you got as a parameter. Per the Immutable.js docs at https://facebook.github.io/immutable-js/docs/#/Map:
If the updater function returns the same value it was called with, then no change will occur. This is still true if notSetValue is provided.
So yeah. You're returning the same value you were given, your direct mutations to it are showing up in the DevTools because that object is still hanging around, but since you returned the same object Immutable.js isn't actually returning any modified objects further up the hierarchy. So, when Redux does a check on the top-level object, it sees nothing has changed, doesn't notify subscribers, and therefore your component's mapStateToProps never runs.
Clean up your reducer and return a new object from inside that updater, and it should all just work.
(A rather belated answer, but I just now saw the question, and it appears to still be open. Hopefully you actually got it fixed by now...)