I have the following array of teams that are being mapped out into Team components:
parent.js
render() {
let teamArray = this.props.teamsReducer.teams;
<ul>
{teamArray.map((team) => {
<Team key={team._id} />
})}
</ul>
}
Team.js
render() {
let getTeamDetailsAnimation = this.props.teamReducer.getTeamDetailsAnimation;
return (
<li className='indent'>
<span className={"cta " + getTeamDetailsAnimation} onClick={() => {this.chooseTeam(team._id,team.name)}}>Select</span>
</li>
)
}
getTeamDetailsAnimation basically gets updated from being an empty string to 'active' which in turn shows a loading spinner while an AJAX request is made. Once the AJAX request is finished getTeamDetailsAnimation gets updated back to an empty string and hence the loading spinner is removed.
The issue i'm having trouble understanding how to tackle is how to apply the getTeamDetailsAnimation to the relevant Team component that was clicked. At the moment seeing as every Team component that is rendered has that part of the teamReducer applied to it, when I click one and an ajax request is made to get the team details then all the Team components show the loading spinner.
I thought about just doing a setState within that Team component to keep the state local to that component but the issue here is my chooseTeam function dispatches an action that lives in a Thunk to make the Ajax request and I need to be able to update the state only when the request is complete which is not local to that component.
As i've setup patterns to keep all my AJAX requests external to my components I want to try and keep it that way, is it possible to keep this AJAX request external to the component given i'm only try to target the relevant component?
Thanks!
You need to store the loading state for individual teams, rather than as a whole
The structure of you teamReducer must be something like
const initialState = [
{
getTeamDetailsAnimation: '',
// other properties
}
]
For simplicity, you can pass the index of the team to get its loading state
Parent:
render() {
let teamArray = this.props.teamsReducer.teams;
<ul>
{teamArray.map((team, index) => {
<Team key={team._id} index={index}/>
})}
</ul>
}
Team:
render() {
let getTeamDetailsAnimation = this.props.teamReducer[this.props.index].getTeamDetailsAnimation;
return (
<li className='indent'>
<span className={"cta " + getTeamDetailsAnimation} onClick={() => {this.chooseTeam(team._id,team.name)}}>Select</span>
</li>
)
}
Related
Problem: Component render starts to drift from actual state
Desired Output: Component render matches state.
So. I'm going to give a bit of a high-level overview with pseudocode as this issue is quite complex, and then I'll show the code.
I have a main form, and this form has an array of filter-states that are renderable in their own components. These filter-states are a one-to-many relationship with the form. The form has-many filter-states.
form: {
filters: [
filter1,
filter2
]
}
Say you want to remove an item from the state, you would do something like so in the reducer (redux)
state.form.filters.filter(f => f.id != action.payload.id)
All good. The state is updated.
Say, you want to render this state, you would do something like so:
// component code ommited, but say you get your form state from redux into the component
formState.filters.map(filter => <FilterComponent filter={filter}/>
All good. your filters are being injected into the component and everyone is happy
Now. This is where it gets weird pretty quickly.
There is a button on my FilterComponent, that says delete. This delete button goes to the reducer, runs the code to delete the filter from the formstate (as you saw above), and yes, it DOES work. The state gets updated, BUT, the UI (the array of components) starts to drift from the state. The UI shows previously deleted states, and states that should be persisted are not shown (but in the redux tab on chrome, the state is CORRECT...!)
The UI acts as if the array of states is being pop()'d; no matter how you remove the states, it will remove the final state in component render.
Now, for the code.
// This takes a list of filters from the form state and loads them into individual form components
const Filters: NextPage<Filters> = () => {
const formState = useSelector((state: any) => state.form.formState)
// In the hope that state change will force reload components, but no avail
useEffect(() => {
console.log("something has been reloaded")
}, [formState])
return (
<>
{formState.form.map((filter, i) => {
return <FilterForm defaultState={filter} key={i} index={i} />
})}
</>
);
};
export default Filters;
The form for these individual states:
Please note, this is obviously redacted a lot but the integral logic is included
const FilterForm: NextPage<FilterFormProps> = ({ defaultState, index }) => {
const formState = useSelector((state: any) => state.form.formState)
// Local component state; there are multiple forms so the state should be localised
const [FilterState, setFilterState] = useState(defaultState)
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const updateParentState = async () => {
dispatch(updateForm(filterState))
}
useEffect(() => {
updateParentState()
}, [filterState])
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} name={filterState.id} />
<Input
name="filter_value"
onChange={handleOnChange} // does standard jazz
value={filterState.filter_value} // standard jazz again
/>
)
}
Now what happens is this: if I click delete, redux updates the correct state, but the components display the deleted state input. Ie, take the following:
filter1: {filter_value: "one"}
filter2: {filter_value: "two"}
filter3: {filter_value: "three"}
these filters are rendered in their own forms.
Say, I click delete on filter1.
filter1 will be deleted from redux, but the UI will show two forms: one for filter1 and one for filter2.
This drift from UI to state baffles me. Obviously I am doing something wrong, can someone spot what it is?!
So, I fixed the issue.
As it turns out, there isnt really an explanation for why the above behaved as it does, but it does warrant for a better implementation.
The issue was as follows; the redux state was conflicting with the local state of the rendered components it was injected in. Why it did, is another story. Somehow, while injecting the redux state into the component and assigning it to the local state, the states went a bit haywire and drifted apart.
The solution was to get rid of the local state (filterState), the updateParentState function call and rather to update the localised state directly through the parent state that it resides in.
The new component looked something like the following:
const FilterForm: NextPage<FilterFormProps> = ({ state, index }) => {
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const handleChange = (e) => {
dispatch(updateFormFilterState({ ...state, [e.target.name]: e.target.value }))
}
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} />
<Input
name="filter_value"
onChange={handleChange}
value={state.filter_value}
/>
)
}
Hope this answer helps someone with the same issue as me.
I'm working on a site where I have a gallery and custom build lightbox. Currently, I'm querying my data with a page query, however, I also use them in other components to display the right images and changing states. It is easier for me to store states in Context API as my data flow both-ways (I need global state) and to avoid props drilling as well.
I've setup my context.provider in gatsby-ssr.js and gatsby-browser.js like this:
const React = require("react");
const { PhotosContextProvider } = require("./src/contexts/photosContext");
exports.wrapRootElement = ({ element }) => {
return <PhotosContextProvider>{element}</PhotosContextProvider>;
};
I've followed official gatsby documentation for wrapping my root component into context provider.
Gallery.js here I fetch my data and set them into global state:
import { usePhotosContext } from "../contexts/photosContext";
const Test = ({ data }) => {
const { contextData, setContextData } = usePhotosContext();
useEffect(() => {
setContextData(data);
}, [data]);
return (
<div>
<h1>hey from test site</h1>
{contextData.allStrapiCategory.allCategories.map((item) => (
<p>{item.name}</p>
))}
<OtherNestedComponents />
</div>
);
};
export const getData = graphql`
query TestQuery {
allStrapiCategory(sort: { fields: name }) {
allCategories: nodes {
name
}
}
}
`;
export default Test;
NOTE: This is just a test query for simplicity
I've double-checked if I get the data and for typos, and everything works, but the problem occurs when I try to render them out. I get type error undefined. I think it's because it takes a moment to setState so on my first render the contextData array is empty, and after the state is set then the component could render.
Do you have any idea how to work around this or am I missing something? Should I use a different type of query? I'm querying all photos so I don't need to set any variables.
EDIT: I've found a solution for this kinda, basically I check if the data exits and I render my component conditionally.
return testData.length === 0 ? (
<div className="hidden">
<h2>Hey from test</h2>
<p>Nothing to render</p>
</div>
) : (
<div>
<h2>Hey from test</h2>
{testData.allStrapiCategory.allCategories.map((item) => (
<p>{item.name}</p>
))}
</div>
);
However, I find this hacky, and kinda repetitive as I'd have to use this in every component that I use that data at. So I'm still looking for other solutions.
Passing this [page queried] data to root provider doesn't make a sense [neither in gatsby nor in apollo] - data duplication, not required in all pages/etc.
... this data is fetched at build time then no need to check length/loading/etc
... you can render provider in page component to pass data to child components using context (without props drilling).
Edit. I rewrote the code to be even more minimalist. The below code is a spike test of my issue.
Here is a video of the issue:
https://imgur.com/a/WI2wHMl
I have two components.
The first component is named TextEditor (and it is a text editor) but its content is irrelevant - the component could be anything. A simple div with text would be just as relevant.
The next component is named Workflows and is used to render a collection from IndexDB (using the Dexie.js library). I named this collection "workflows" and the state variable I store them in is named workflows_list_array
What I am trying to do is the following:
When the page loads, I want to check if any workflows have a specific ID . If they do, I store them in workflows_list_array and render them. I don't need help with this part.
However, if no workflows with the aforementioned criteria exist, I want to keep the component named Workflows hidden and render the component named TextEditor. If workflows do exist, I want the TextEditor hidden and to display Workflows
The problem is that even though I have it working, when workflows do exist (when workflows_list_array is populated) the TextEditor "flickers" briefly before being hidden and then the Workflows component is displayed.
I can tell this is an async issue but I can't tell how to fix it.
I posted code below and I tried to keep it to a minimum.
Test.js
import React, {useState, useEffect} from "react";
import db from "../services"
function Workflows(props){
return (
<div>
<ul>
{
props.workflows.map((val,index)=>{
return <li key={index}>{val.content}</li>
})
}
</ul>
</div>
)
}
function TextEditor(){
return (
<div> TextEditor </div>
)
}
function Test(props){
let [workflows_list_array, set_state_of_workflows_list_array] = useState([]);
let [client_id_number, set_client_id_number] = useState(5);
useEffect(() => { // get all workflows of the selected client per its ID
db.workflows.toArray((workflows_list)=>{ // iterate through workflows array
return workflows_list
}).then((workflows_list)=>{
workflows_list.forEach((val)=>{
if(client_id_number === val.client_id){
set_state_of_workflows_list_array((prev)=>{
return [...prev, val]
});
}
});
});
}, []);
return(
<div>
{workflows_list_array.length ? null : <TextEditor/> }
{workflows_list_array.length ? <Workflows workflows={workflows_list_array}/> : null}
</div>
)
}
export default Test
services.js
import Dexie from 'dexie';
import 'dexie-observable';
var workflowMagicUserDB = new Dexie("WorkflowMagicUserDB");
workflowMagicUserDB.version(1).stores({
user: "",
workflows: "++id,client_id,content,title",
clients: "++id,name",
calendar_events: "++id,start,end,title"
});
export default workflowMagicUserDB
Why don't you include a flag which indicates if you have already got data from IndexDB, something like:
function Test(props){
const [loading, setLoading] = React.useState(true);
let [workflows_list_array, set_state_of_workflows_list_array] = useState([]);
let [client_id_number, set_client_id_number] = useState(5);
useEffect(() => {
db.workflows.toArray((workflows_list)=>{
}).then((workflows_list)=>{
}).finally(() => setLoading(false)); //when process finishes, it will update the state, at that moment it will render TextEditor or Workflows
}, []);
if(loading) return <LoadingIndicator/>; // or something else which indicates the app is fetching or processing data
return(
<div>
{workflows_list_array.length ? null : <TextEditor/> }
{workflows_list_array.length ? <Workflows workflows={workflows_list_array}/> : null}
</div>
)
}
export default Test
When the process finishes, finally will be executed and set loading state to false, after that, your app will render TextEditor or Workflows
I am new to react. I have created a news component that consumes a json url then spits out some news articles. In the client side UI if the clients changes the json url it will update without refreshing the page using this code:
componentDidUpdate(prevProps) {
if (prevProps.jsonUrl !== this.props.jsonUrl) {
this.getPosts();
}
}
However I also need the the news feed to update reactively if the postCount: this.props.postCount is changed in the client side UI. The post count is used in the render method below to choose how many posts to display.
posts
.slice(0, postCount)
.map(post => {
// Variables to use
let { id, name, summary, url, imgUrl} = post;
// Stripping html tags from summary
//let strippedSummary = summary.replace(/(<([^>]+)>)/ig,"");
// What we actually render
return (
<div key={id} className={ styles.post}>
<p>{name}</p>
{/* <p>{summary}</p> */}
<a href={url}>{url}</a>
<img className={ styles.postImage} src={imgUrl} />
</div>
);
})
Any help is much appreciated! - I was thinking something like this inside componentDidUpdate:
if (prevProps.postCount !== this.props.postCount) {
this.setState( this.state.postCount; );
}
EDIT:
I am now using the postCount from the props instead of a state and it updates instantly! :D
// Grabbing objects to use from state
const { posts, isLoading } = this.state;
const { postCount } = this.props;
The components are going to react automatically to the changes in their props, so there's no need to transfer any props to a state. In this case, if postCount is a prop, when it changes it should affect the piece of code that you shared to render the component. However, I don't know if posts is part of the state, in your case it should be and your method getPosts should setState with the new posts.
I'm trying to figure out how to populate/render a component when the data is ready? Essentially I have a script that queries my server which returns data, then I parse it and make it into an collection with the properties I need. Then in another file, I have the react component that's looking for that object but they're running at the same time so the object doesn't exist when the component is looking for it.
I'm not sure how to proceed.
This is my component:
let SliderTabs = React.createClass({
getInitialState: function() {
return { items: [] }
},
render: function() {
let listItems = this.props.items.map(function(item) {
return (
<li key={item.title}>
{item.title}
</li>
);
});
return (
<div className="something">
<h3>Some content</h3>
<ul>
{listItems}
</ul>
</div>
);
}
});
ReactDOM.render(<SliderTabs items={home.data.slider} />,
document.getElementById('slider-tabs'));
How I'm getting my data:
var home = home || {};
home = {
data: {
slider: [],
nav: []
},
get: function() {
var getListPromises = [];
$.each(home.lists, function(index, list) {
getListPromises[index] = $().SPServices.SPGetListItemsJson({
listName: home.lists[index].name,
CAMLViewFields: home.lists[index].view,
mappingOverrides: home.lists[index].mapping
})
getListPromises[index].list = home.lists[index].name;
})
$.when.apply($, getListPromises).done(function() {
home.notice('Retrieved items')
home.process(getListPromises);
})
},
process: function(promiseArr) {
var dfd = jQuery.Deferred();
$.map(promiseArr, function(promise) {
promise.then(function() {
var data = this.data;
var list = promise.list;
// IF navigation ELSE slider
if (list != home.lists[0].name) {
$.map(data, function(item) {
home.data.nav.push({
title: item.title,
section: item.section,
tab: item.tab,
url: item.url.split(",")[0],
path: item.path.split("#")[1].split("_")[0]
})
})
} else {
$.map(data, function(item) {
home.data.slider.push({
title: item.title,
url: item.url.split(",")[0],
path: item.path.split("#")[1]
})
})
}
})
})
console.log(JSON.stringify(home.data))
dfd.resolve();
return dfd.promise();
}
}
$(function() {
home.get()
})
A common way to do this in React is to keep track of when data is being fetched. This can be done e.g. by having a isFetching field in your state:
// This would be your default state
this.state = {
isFetching: false
};
Then, when you fire off the request (preferably in componentDidMount) you set isFetching to true using:
this.setState({ isFetching: true });
And finally, when the data arrives, you set it to false again:
this.setState({ isFetching: false });
Now, in your render function you can do something like this:
render () {
return (
<div className="something">
<h3>Some content</h3>
{this.state.isFetching ? <LoadingComponent /> : (
<ul>
{listItems}
</ul>
)}
</div>
)
}
By using state, you don't have to worry about telling your component to do something, instead it reacts to changes in the state and renders it accordingly.
Update:
If you plan on actually using React, I'd suggest you change your approach into what I've described above (read more in the React docs). That is, move the code you have in your get function into your React component's componentDidMount function. If that's not possible, you can probably just wait to call
ReactDOM.render(
<SliderTabs items={home.data.slider} />,
document.getElementById('slider-tabs')
);
until your data has arrived.
Here is the explaination of React's way of doing these type of things, tl;dr - render the component immediately and either display loading indicator until the data is ready or return null from the render method.
Put that data loading in parent component that updates the props of your component as the data is being loaded.
Use default props instead of default state, since you are not using state at all in your example. Replace the 'getInitialState' with this:
getDefaultProps: function() {
return {
items: []
};
}
You should test the length of the data collection. If the collection is empty, return a placeholder (a loading wheel for exemple). In other cases, you can display the data collection as usual.
const SliderTabs = ({items}) => {
let listItems = <p>Loading data...</p>
if(items.length != 0)
listItems = items.map(item =>
<li key={item.title}>
{item.title}
</li>
)
return (
<div className="something">
<h3>Some content</h3>
<ul>
{listItems}
</ul>
</div>
)
}
ReactDOM.render(
<SliderTabs items={home.data.slider} />,
document.getElementById('slider-tabs')
)
I have use the functional way to define a React Component as it's the recommended way while you don't need of a state, refs or lifecycle methodes.
If you want to use this in a ES6 classe or with React.createCompnent (shoud be avoid), just use the function as the render function. (don't forget to extract items form the props)
EDIT : By reading the new answers, I've realised that I haven't fully answered.
If you want the view to be updated when the data are loaded, You have to integrate a little more your data fetching code. A basic pattern in React is to separate your components in tow type : the Containers Component and the Presentational Component.
The Containers will only take care of the logic and to fetch the useful data. In the other hand, the Presentational Components will only display the data given by the Container.
Here a little example : (try it on jsfidle)
Test utilities
var items = [{title: "cats"},{title: "dogs"}]
//Test function faking a REST call by returning a Promise.
const fakeRest = () => new Promise((resolve, reject) =>
setTimeout(() => resolve(items), 2000)
)
Container Component
//The Container Component that will only fetch the data you want and then pass it to the more generic Presentational Component
class SliderTabList extends React.Component {
constructor(props) { //
super(props)
//You should always give an initial state (if you use one of course)
this.state = { items : [] }
}
componentDidMount() {
fakeRest().then(
items => this.setState({ items }) //Update the state. This will make react rerender the UI.
)
}
render() {
//Better to handle the fact that the data is fetching here.
//This let the Presentational Component more generic and reusable
const {items} = this.state
return (
items.length == 0
? <p>Loading Data...</p>
: <List items={items} />
)
}
}
Presentational Component
//The Presenational Component. It's called List because, in fact, you can reuse this component with other Container Component. Here is power of React.
const List = ({items}) => {
//Prepare the rendering of all items
const listItems = items.map(item =>
<li key={item.title}>
{item.title}
</li>
)
//Simply render the list.
return (
<div className="something">
<h3>Some content</h3>
<ul>
{listItems}
</ul>
</div>
)
}
Rendering the App
//Mount the Container Component. It doesn't need any props while he is handling his state itself
ReactDOM.render(
<SliderTabList />,
document.getElementById('slider-tabs')
)
Rather than checking for the length to not being 0, you also can initialise items to null in the state, to be able to differentiate fetching data from empty data. An other common way os to put a flag (a boolean in fetchingData int the state) to know if a data is fetching or not. But, in lots of articles, it's generaly recomended to have a state as litle as possible and then calculate all you need from it. So here, I sugest you to check for the length or the null.
I got a few answers but I was still having a lot of trouble understanding how to accomplish what I was asking for. I understand that I should be retrieving the data with the components but I currently don't know enough about React to do that. I obviously need to spend more time learning it but for now I went with this:
Essentially, I added the property ready to my home object:
home.ready: false,
home.init: function() {
// check if lists exist
home.check()
.then(home.get)
.then(function() {
home.ready = true;
})
}
Then I used componentDidMount and a setTimeout to check when the data is ready and set the results to the this.state
let SliderTabs = React.createClass({
getInitialState: function() {
return {items:[]}
},
componentDidMount: function() {
let that = this;
function checkFlag() {
if(home.ready == false) {
window.setTimeout(checkFlag, 100); /* this checks the flag every 100 milliseconds*/
} else {
that.setState({items: home.data.slider})
}
}
checkFlag();
},
render: function() {
let listItems = this.state.items.map(function(item) {
return (
<li key={item.title}>
{item.title}
</li>
);
});
return (
<div className="something">
<h3>Some content</h3>
<ul>
{listItems}
</ul>
</div>
);
}
});
ReactDOM.render(<SliderTabs/>,
document.getElementById('slider-tabs'));
Probably not the React way but it seems to work.
Ordinarily, you would arrange a response to an OnLoad event, which will "fire" once the data has been loaded.
I agree with Emrys that your code should also be prepared to test whether the data is available yet, and to "do something reasonable" on the initial draw (when it probably isn't). But no, you would not then "poll" to see if the data has arrived: instead, you arrange to be notified when the event fires. At that time, you would (for example) re-draw the UI components to reflect the information.
I kindly refer you to the React documentation to see exactly how this notification is done ...