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.
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).
i am fighting with a react app with a movie API (https://developers.themoviedb.org) hahaha. I have a list component with movie and tv cards. I map a list state and put the info in my cards (id, title and poster). When i click in my card, the site must show movie information. So, i have another component named movies.jsx and there i have another three components: hero.jsx (who contains a img from the movie), menuHero.jsx (a menu) and movieInfo.jsx (that contains the title, a little desc from the movie and blabla). I need fill this three components with the api info.
My component movies.jsx show me the id (i put that in a < h2> just for see if its working) but i don't find a way to give them the api info to my another three child components.
Also, the movieData its empty. So, i dont know what im doing wrong.
Here is my code:
const Movies = props => {
const [movieData, setMovieData] = useState([]);
const { match } = props;
const movieId = match.params.id;
useEffect(() => {
axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=${api-key}`)
.then(res => {
setMovieData(res.data);
console.log(movieData)
}).catch(error => console.log(error))
}, []);
return(
<div className="container-section">
<h2>{match.params.id}</h2>
<Hero />
<MenuHero />
<MovieInfo />
</div>
)
}
export default Movies;
enter code here
You can pass that data as props:
return movieData && (
<div className="container-section">
<h2>{match.params.id}</h2>
<Hero movieData={movieData} />
<MenuHero movieData={movieData} />
<MovieInfo movieData={movieData} />
</div>
)
More info: https://reactjs.org/docs/components-and-props.html
You need to get an API key and movie ID from themoviedb, ${movieId} ${api-key} should be your key which should be stored as an environment variable. (.env) file and to access that key you'll need to use process.env.(VARIABLE_NAME) to get the desired value. That's why you're not getting any data from your get requests.
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>
)
}
I'm making a infinite scroll in ReactJs with posts.
I have a react class called AllPosts and Post. AllPosts render multiple Posts.
I have this code:
ReactDOM.render(
<AllPosts data={posts} />,
document.querySelector(render_to)
);
And below is the method
// Render all posts
var AllPosts = React.createClass({
render: function () {
return (
<div>
{this.props.data.map(function(element, i) {
return <Post data={element} />
})}
</div>
); .....
But, I have an event in scroll and I want to append another react Post. How can I do this?
This is one of those awesome things React is great at :)
On the assumption that you don't want to use a Flux/Redux implementation, I would set the posts data as the state on your root component. That way, when posts changes, the component will re-render:
var AllPosts = React.createClass({
getInitialState() {
// Start with an empty array of posts.
// Ideally, you want this component to do the data fetching.
// If the data comes from a non-react source, as in your example,
// you can do `posts: this.props.posts`, but this is an anti-pattern.
return { posts: [] }
},
componentWillMount() {
// Fetch the initial list of posts
// I'm assuming here that you have some external method that fetches
// the posts, and returns them in a callback.
// (Sorry for the arrow functions, it's just so much neater with them!)
ajaxMethodToFetchPosts(posts => {
this.setState({ posts: posts });
})
},
componentDidMount() {
// Attach your scroll handler here, to fetch new posts
// In a real example you'll want to throttle this to avoid too many requests
window.addEventListener('scroll', () => {
ajaxMethodToFetchPosts( posts => {
this.setState({
posts: this.state.posts.slice().concat(posts)
});
});
});
},
render() {
// Render method unchanged :)
return (
<div>
{this.props.data.map(function(element, i) {
return <Post data={element} />
})}
</div>
);
}
});
With other frameworks, you have to deal with scroll position (if the element gets completely re-rendered, the elements disappear momentarily and your scroll position is reset).
React's render function doesn't actually just render its output to the DOM; it compares the potential output with what's already there, and only applies the difference. This means that only new posts are added to the DOM, and your scroll position will remain unaffected.