I built a simple reddit app with React-Redux and when I go to a post page like /posts/mlxft5 and refresh the page it says that the post is not found. I can't figure out how to fix it. Here's my code on codesandbox.
here's my app.js code
function App() {
return (
<Router >
<div className="App">
<div className="App-navbar">
<SubredditsList />
</div>
<Switch>
<Route exact path="/" component={StartMessage}/>
<Route exact path="/search" component={SearchPostsList}/>
<Route exact path="/posts" component={PostsList} />
<Route path="/posts/:postId" component={PostDetailRender}/>
<Route path="/search/:postId" component={SingleSearchPostRender}/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
export default App`
Right now you are only requesting posts from the API when we are on the /posts page for a subreddit. There are no requests initiated by /posts/mlxft. We need to add an additional thunk action that can fetch and store a single post from the id.
We don't want to fetch posts that are already in Redux when we click on a single post from the list on the /posts page. We we will use the condition setting of the createAsyncThunk function to conditionally cancel the fetch if the data already exists.
export const fetchSinglePost = createAsyncThunk(
"posts/fetchSingle",
async (postId) => {
const response = await fetch(
`https://api.reddit.com/api/info/?id=t3_${postId}`
);
const json = await response.json();
return json.data.children[0].data;
},
{
condition: (postId, { getState }) => {
const { posts } = getState();
if (posts.entities[postId]) {
return false;
}
}
}
);
You need to add additional cases in your reducer to handle this thunk. Note: if you use builder callback notation instead of reducer map object notation then you could combine your two "rejected" cases.
[fetchSinglePost.pending]: (state, action) => {
state.status = "loading";
},
[fetchSinglePost.fulfilled]: (state, action) => {
state.status = "succeeded";
postsAdapter.upsertOne(state, action.payload);
},
[fetchSinglePost.rejected]: (state, action) => {
state.status = "failed";
state.error = action.error.message;
}
Inside of your PostDetailRender component you need to dispatch the fetchSinglePost action. It's ok to dispatch it all cases because the thunk itself will cancel the fetching.
useEffect(() => {
dispatch(fetchSinglePost(postId));
}, [dispatch, postId]);
You could potentially have a status for each post rather than one for the whole slice. I explain how to do that in this answer.
Updated CodeSanbox
I also made some changes so that you don't fetch the same subreddit's posts more than once.
Related
I need to show the text according to the data value. By running the code, I want to see the 'Test1: 1' can be shown after I clicked the button, but I can't. Any method to make this happen?
Below is a sample sandbox link including the code.
https://codesandbox.io/s/restless-wildflower-9pl09k?file=/src/Parent.js
export default function Parent(props) {
const [data, setData] = useState(0);
const onClick = () => {
setData(1);
console.log(data);
setData(2);
};
return (
<>
<button onClick={onClick}> Click here </button>
{data === 1 ? <div>Test1: {data}</div> : <div>Test2: {data}</div>}
</>
);
}
The setState function returned by useState does not directly update the state. Instead it is used to send the value that React will use during the next asynchronous state update. console.log is an effect so if you want to see data logged every time it is changed, you can use React.useEffect. Run the code below and click the 0 button several times to see the state changes and effects in your browser.
function App() {
const [data, setData] = React.useState(0)
React.useEffect(_ => console.log("data", data), [data])
return <button
onClick={_ => setData(data + 1)}
children={data}
/>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Your comment talks about a network request example. Custom hooks can be designed to accommodate complex use cases and keep your components easy to write.
function App() {
const [{fetching, error, data}, retry] = useAsync(_ => {
return fetch("https://random-data-api.com/api/users/random_user")
.then(res => res.json())
}, [])
if (fetching) return <pre>Loading...</pre>
if (error) return <pre>Error: {error.message}</pre>
return <div>
<button onClick={retry} children="retry" />
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
}
function useAsync(f, deps) {
const [state, setState] = React.useState({fetching: true})
const [ts, setTs] = React.useState(Date.now())
React.useEffect(_ => {
f()
.then(data => setState({fetching: false, data}))
.catch(error => setState({fetching: false, error}))
}, [...deps, ts])
return [
state,
_ => {
setState({fetching: true, error: null, data: null})
setTs(Date.now())
}
]
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
The reason the console.log(data) did not reflect the latest data is because of the React manages state. Calls to setState() are asynchronous, and if you want to rely on the new value of the state, the correct way is to pass a function of old state, returning the current state. ref. documentation
I am having an issue where it seems my state variable is being overwritten by the default/initial value I set when calling useState(). I have const [token, setToken] = useState(""). I pass in the setToken function to my Login component, and when I log in I update the token via the setToken function.
In my App.js I only want to render the Login component if the token is not set, otherwise, I redirect to a Post.js component. Once I log in my token is being set and I am being redirected to my Posts.js page, the problem is if I refresh the page the token is overwritten again to the initial value and I'm not sure why that is.
My understanding is that useState() is only called on the initial render, otherwise the state is obtained from whatever is stored in state which since I called the setToken function, should be the state with the token, not an empty string.
What am I missing?
App.js
function App() {
const [errors, setErrors] = useState([]);
const [token, setToken] = useState("");
console.log('token', token)
return (
<Fragment>
<Router>
<Navbar />
<Route exact path="/" component={Landing} />
<Container>
{!errors.length ? <></> : <Alerts errors={errors} handleError={setErrors}/> }
<Switch>
<Route
exact path="/login"
render={ token ? (props) => <Redirect push to="/posts" {...props} /> : (props) => (<Login error={setErrors} isLoggedIn={setToken} {...props} />)}
/>
<Route exact path="/posts"
render={token ? (props) => <Posts token={token} {...props} /> : (props) => <Redirect push to="/login" {...props} />} />
</Switch>
</Container>
</Router>
</Fragment>
);
}
Pertinent section of Landing.js
const handleSubmit = async (e) => {
e.preventDefault();
const res = await login(email, password);
if(res.status !== 200){
props.error(res.data.errors);
}else{
props.error([])
props.isLoggedIn(res.data.token)
}
}
UPDATE:
I did figure out how to do what I wanted to do, by storing my token in local storage. In my App.js I'm doing this now
function App() {
const [errors, setErrors] = useState([]);
const [token, setToken] = useState(null);
useEffect(() => {
setToken(window.localStorage.getItem("token"));
})
This is not overwriting my token state as it was initially, so it's doing what I want, but I'm still wondering what I was doing wrong in my original implementation?
when you refresh the page your app runs from the beginning. If you don't want to lose your token on page refresh you should save/retrieve your to/from localStorage.
We have implemented bot framework-webchat to create a bot. Currently, we handle the minimize and maximize with the event passed in component (code shown below) but the challenge occurs when I minimize and then maximize the chatbot I am seeing 'Unable to connect' message and then it flashes away and if after an hour-long later if we minimize and maximize I am getting 'Network interruption occurred, Reconnecting...' How do I keep webchat potentially automatically reconnect when I minimize and maximize Chabot.
MaximizeChatWndow () {
if (this.state.token === null &&
this.state.productService === null) {
return
}
this.setState({
directLine: this.createDirectLine()
}, () => {
this.setState({
minimized: false,
newMessage: false,
userId: 'User_' + Math.random
})
})
this.checkExpandFlag = true
}
The component:
render () {
const {
state: { minimized, store }
} = this
return (
<Row>
<Col md={12}>
<div>
{minimized ? (
<ChatDon
handleMaximizeButtonClick={this.handleMaximizeButtonClick}
/>
) : (
<ChatWin
handleMinimizeButtonClick={this.handleMinimizeButtonClick}
directLine={this.state.directLine}
userId={this.state.userId}
store={store}
/>
)}
</div>
</Col>
</Row>
)
}
It looks like you creating your directLine object in "MaximizeChatWndow()" which I think is the problem. In "MaximizeChatWndow()", you should be fetching your token and passing that to your web chat component. It is in the web chat component that you should use the token to call createDirectLine().
It appears that there have been various updates to the 06.recomposing-us/a.minimizable-web-chat sample. (The docs also look like they are out of date and no longer match the code). However, if comparing to the available sample code, you will want to do something like the following. Please look at the full code in the above link as I am only including the most relevant parts here.
When I tested, I had no issues with the conversation resetting or the network disconnecting/reconnecting.
MinimizableWebChat.js
import WebChat from './WebChat';
const MinimizableWebChat = () => {
[...]
const [token, setToken] = useState();
const handleFetchToken = useCallback(async () => {
if (!token) {
const res = await fetch('http://localhost:3500/directline/token', { method: 'POST' });
const { token } = await res.json();
setToken(token);
}
}, [setToken, token]);
[...]
return (
[...]
<WebChat
className="react-web-chat"
onFetchToken={handleFetchToken}
store={store}
styleSet={styleSet}
token={token}
/>
)
WebChat.js
const WebChat = ({ className, onFetchToken, store, token }) => {
const directLine = useMemo(() => createDirectLine({ token }), [token]);
[...]
useEffect(() => {
onFetchToken();
}, [onFetchToken]);
return token ? (
<ReactWebChat ...
);
};
Hope of help!
I have an application with one subscription already using subscribeToMore
Query Component:
<Query query={GET_MESSAGES}>
{({ data, subscribeToMore }) => {
return (
<MessageList
messages={data.allMessages}
subscribeToMore={subscribeToMore}/>
);
}}
</Query>
That query loads a list of messages where as far as I know we attach the subscriber in the ComponentDidMount so whenever I add a new element into my list inside the Query, my subscription will listen to the subscription and do whatever I want (in this case add the new message into my current list of messages).
List of Messages Component:
export default class MessageList extends React.Component {
componentDidMount() {
this.props.subscribeToMore({
document: MESSAGE_CREATED,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
return {
allMessages: [
subscriptionData.data.messageCreated,
...prev.allMessages
],
};
},
});
}
render() {
return (
<div>
{this.props.messages.map(message => (
<MessageElement key={message.id}
message={message}/>
))}
</div>
);
}}
I would like to add another subscription so if I edit my message the information is updated in real time. In order to achieve that I have created the following component using Subscription
Message component (where I would like to add another subscription based on an updated message)
export default class MessageElement extends Component{
componentDidMount() {
this.messageUpdatedSubscription();
}
messageUpdatedSubscription = () => (
<Subscription
subscription={MESSAGE_UPDATED}>
{({ data: { messageUpdated } }) => (
console.log('Do smthg???',messageUpdated)
)}
</Subscription>
);
render(){
return(
<Mutation mutation={UPDATE_MESSAGE} key={this.props.message.id}>
{updateMessage => (
<div>
<div>{this.props.message.text}</div>
<div>
<Star active={this.props.message.isFavorite}
onClick={() => { updateMessage({ variables: {
id: this.props.message.id,
text:this.props.message.text,
isFavorite:!this.props.message.isFavorite } });
}}/>
</div>
</div>
)}
</Mutation>);
}}
My subscriptions on the server are working as I already have the subscription for MESSAGE_CREATED on the Query working correctly and I have tested that on the server my subscription for the MESSAGE_UPDATED is triggered. However, I cannot figure out why the UI is not displaying or console.log anything as if it is not listening to the subscription.
Can I achieve this with a subscription component or with a subscribeToMore?
Thanks in advance
The subscription component cannot be initiated in ComponentDidMount. It has to reside in a render() lifecycle method. It's parameter can be used in ComponentDidMount, but not the component.
I can think of 3 possible solutions:
1) You could try to put the Subscription method in your render method. You would just need to nest this inside or outside of your Mutation component.
2) You could initiate the Subscription Component in this component's parent and pass its parameter (messageUpdate) down to the component MessageElement. You could then use messageUpdate off of props in ComponentDidMount.
3) You could use Apollo's higher order component. You could then access messageUpdate in props. (Disclaimer - I have not tried this before).
I hope this helps!
Based on the suggestion of #cory-mcaboy I nested my subscription into the mutation.
I also found out that as I had a list of messages and I just wanted to trigger the subscription based on the message I am updating and not the entire list; I had to modified my subscription on the backend and on the front end in the following way:
Server schema
const typeDefs = gql` type Subscription {
messageUpdated(id: Int!): Message}`;
Server resolver
Subscription: {
messageUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator([MESSAGE_UPDATED]),
(payload, variables) => {
return payload.messageUpdated.id === variables.id;
},
),
},
}
Client component
const MESSAGE_UPDATED = gql` subscription messageUpdated($id: Int!){
messageUpdated(id:$id)
{
id
text
isFavorite
}}`;
export default class MessageElement extends Component{
render(){
return(<Mutation mutation={UPDATE_MESSAGE} key={this.props.message.id}>
{updateMessage => (
<div>
<div>{this.props.message.text}</div>
<Subscription subscription={MESSAGE_UPDATED}
variables={{ id: this.props.message.id }}>
{() => {
return <Star active={this.props.message.isFavorite}
onClick={() => { updateMessage({ variables: {
id: this.props.message.id,
text: this.props.message.text,
isFavorite: !this.props.message.isFavorite } });}}/>
}}
</Subscription>
</div>
)}
</Mutation>
);
}}
You can see the code in the following repo: back-end itr-apollo-server, front-end itr-apollo-client
I saw a lot of users struggling to use useSubscription in Class Component as Apollo deprecated the Subscription component in the newer version. So, I decided to create quick gist of how to use it.
https://gist.github.com/syedadeel2/a9ec6ff0d9efade2b83d027f32ce21dc
On the official pages and in the GitHub issues for redux-form there are more than one example of how to work with initialValues however I cannot find a single one that focuses on explaining how initialValues can be set in response to an asynchronous source.
The main case that I have in mind is something like a simple CRUD application where a user is going to edit some entity that already exists. When the view is first opened and the redux-form component is mounted but before the component is rendered the initialValues must be set. Lets say that in this example that the data is loaded on demand when the component is first mounted and rendered for the first time. The examples show setting initialValues based on hard coded values or the redux store state but none that I can find focus on how to set the initialValues based on something async like a call to XHR or fetch.
I'm sure I'm just missing something fundamental so please point me in the right direction.
References:
Initializing Form State
Handling form defaults
What is the correct way to populate a dynamic form with initial data?
EDIT: Updated Solution from ReduxForm docs
This is now documented in the latest version of ReduxForm, and is much simpler than my previous answer.
The key is to connect your form component after decorating it with ReduxForm. Then you will be able to access the initialValues prop just like any other prop on your component.
// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
InitializeFromStateForm = reduxForm({
form: 'initializeFromState'
})(InitializeFromStateForm)
// now set initialValues using data from your store state
InitializeFromStateForm = connect(
state => ({
initialValues: state.account.data
})
)(InitializeFromStateForm)
I accomplished this by using the redux-form reducer plugin method.
The following demos fetching async data and pre-populating a user form with response.
const RECEIVE_USER = 'RECEIVE_USER';
// once you've received data from api dispatch action
const receiveUser = (user) => {
return {
type: RECEIVE_USER,
payload: { user }
}
}
// here is your async request to retrieve user data
const fetchUser = (id) => dispatch => {
return fetch('http://getuser.api')
.then(response => response.json())
.then(json => receiveUser(json));
}
Then in your root reducer where you include your redux-form reducer you would include your reducer plugin that overrides the forms values with the returned fetched data.
const formPluginReducer = {
form: formReducer.plugin({
// this would be the name of the form you're trying to populate
user: (state, action) => {
switch (action.type) {
case RECEIVE_USER:
return {
...state,
values: {
...state.values,
...action.payload.user
}
}
default:
return state;
}
}
})
};
const rootReducer = combineReducers({
...formPluginReducer,
...yourOtherReducers
});
Finally you include you combine your new formReducer with the other reducers in your app.
Note The following assumes that the fetched user object's keys match the names of the fields in the user form. If this is not the case you will need to perform an additional step on the data to map fields.
By default, you may only initialize a form component once via initialValues. There are two methods to reinitialize the form component with new "pristine" values:
Pass a enableReinitialize prop or reduxForm() config parameter set to true to allow the form the reinitialize with new "pristine" values every time the initialValues prop changes. To keep dirty form values when it reinitializes, you can set keepDirtyOnReinitialize to true. By default, reinitializing the form replaces all dirty values with "pristine" values.
Dispatch the INITIALIZE action (using the action creator provided by redux-form).
Referenced from : http://redux-form.com/6.1.1/examples/initializeFromState/
Could you fire the dispatch on componentWillMount(), and set the state to loading.
While it is loading, render a spinner for example and only when the request returns with the values, update the state, and then re-render the form with the values??
Here is minimal working example on how to set initialValues based on async source.
It uses initialize action creator.
All values from initialValues shouldn't be undefined, or you will get an infinite loop.
// import { Field, reduxForm, change, initialize } from 'redux-form';
async someAsyncMethod() {
// fetch data from server
await this.props.getProducts(),
// this allows to get current values of props after promises and benefits code readability
const { products } = this.props;
const initialValues = { productsField: products };
// set values as pristine to be able to detect changes
this.props.dispatch(initialize(
'myForm',
initialValues,
));
}
While this method may not be the best solution, it works well enough for my needs:
AJAX request to API on entry
Initializes form with data when request has been fulfilled or displays a server error
Resetting form will still reset to initial seed data
Allows the form to be reused for other purposes (for example, a simple if statement could bypass setting initial values): Add Post and Edit Post or Add Comment and Edit Comment...etc.
Data is removed from Redux form on exit (no reason to store new data in Redux since it's being re-rendered by a Blog component)
Form.jsx:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { browserHistory, Link } from 'react-router';
import { editPost, fetchPost } from '../../actions/BlogActions.jsx';
import NotFound from '../../components/presentational/notfound/NotFound.jsx';
import RenderAlert from '../../components/presentational/app/RenderAlert.jsx';
import Spinner from '../../components/presentational/loaders/Spinner.jsx';
// form validation checks
const validate = (values) => {
const errors = {}
if (!values.title) {
errors.title = 'Required';
}
if (!values.image) {
errors.image = 'Required';
}
if (!values.description) {
errors.description = 'Required';
} else if (values.description.length > 10000) {
errors.description = 'Error! Must be 10,000 characters or less!';
}
return errors;
}
// renders input fields
const renderInputField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<input {...input} className="form-details complete-expand" placeholder={label} type={type}/>
{touched && error && <div className="error-handlers "><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
</div>
</div>
)
// renders a text area field
const renderAreaField = ({ textarea, input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<textarea {...input} className="form-details complete-expand" placeholder={label} type={type}/>
{touched && error && <div className="error-handlers"><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
</div>
</div>
)
class BlogPostForm extends Component {
constructor() {
super();
this.state = {
isLoaded: false,
requestTimeout: false,
};
}
componentDidMount() {
if (this.props.location.query.postId) {
// sets a 5 second server timeout
this.timeout = setInterval(this.timer.bind(this), 5000);
// AJAX request to API
fetchPost(this.props.location.query.postId).then((res) => {
// if data returned, seed Redux form
if (res.foundPost) this.initializeForm(res.foundPost);
// if data present, set isLoaded to true, otherwise set a server error
this.setState({
isLoaded: (res.foundPost) ? true : false,
serverError: (res.err) ? res.err : ''
});
});
}
}
componentWillUnmount() {
this.clearTimeout();
}
timer() {
this.setState({ requestTimeout: true });
this.clearTimeout();
}
clearTimeout() {
clearInterval(this.timeout);
}
// initialize Redux form from API supplied data
initializeForm(foundPost) {
const initData = {
id: foundPost._id,
title: foundPost.title,
image: foundPost.image,
imgtitle: foundPost.imgtitle,
description: foundPost.description
}
this.props.initialize(initData);
}
// onSubmit => take Redux form props and send back to server
handleFormSubmit(formProps) {
editPost(formProps).then((res) => {
if (res.err) {
this.setState({
serverError: res.err
});
} else {
browserHistory.push(/blog);
}
});
}
renderServerError() {
const { serverError } = this.state;
// if form submission returns a server error, display the error
if (serverError) return <RenderAlert errorMessage={serverError} />
}
render() {
const { handleSubmit, pristine, reset, submitting, fields: { title, image, imgtitle, description } } = this.props;
const { isLoaded, requestTimeout, serverError } = this.state;
// if data hasn't returned from AJAX request, then render a spinner
if (this.props.location.query.postId && !isLoaded) {
// if AJAX request returns an error or request has timed out, show NotFound component
if (serverError || requestTimeout) return <NotFound />
return <Spinner />
}
// if above conditions are met, clear the timeout, otherwise it'll cause the component to re-render on timer's setState function
this.clearTimeout();
return (
<div className="col-sm-12">
<div className="form-container">
<h1>Edit Form</h1>
<hr />
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<Field name="title" type="text" component={renderInputField} label="Post Title" />
<Field name="image" type="text" component={renderInputField} label="Image URL" />
<Field name="imgtitle" component={renderInputField} label="Image Description" />
<Field name="description" component={renderAreaField} label="Description" />
<div>
<button type="submit" className="btn btn-primary partial-expand rounded" disabled={submitting}>Submit</button>
<button type="button" className="btn btn-danger partial-expand rounded f-r" disabled={ pristine || submitting } onClick={ reset }>Clear Values</button>
</div>
</form>
{ this.renderServerError() }
</div>
</div>
)
}
}
BlogPostForm = reduxForm({
form: 'BlogPostForm',
validate,
fields: ['name', 'image', 'imgtitle', 'description']
})(BlogPostForm);
export default BlogPostForm = connect(BlogPostForm);
BlogActions.jsx:
import * as app from 'axios';
const ROOT_URL = 'http://localhost:3001';
// submits Redux form data to server
export const editPost = ({ id, title, image, imgtitle, description, navTitle }) => {
return app.put(`${ROOT_URL}/post/edit/${id}?userId=${config.user}`, { id, title, image, imgtitle, description, navTitle }, config)
.then(response => {
return { success: response.data.message }
})
.catch(({ response }) => {
if(response.data.deniedAccess) {
return { err: response.data.deniedAccess }
} else {
return { err: response.data.err }
}
});
}
// fetches a single post from the server for front-end editing
export const fetchPost = (id) => {
return app.get(`${ROOT_URL}/posts/${id}`)
.then(response => {
return { foundPost: response.data.post}
})
.catch(({ response }) => {
return { err: response.data.err };
});
}
RenderAlert.jsx:
import React, { Component } from 'react';
const RenderAlert = (props) => {
const displayMessage = () => {
const { errorMessage } = props;
if (errorMessage) {
return (
<div className="callout-alert">
<p>
<i className="fa fa-exclamation-triangle" aria-hidden="true"/>
<strong>Error! </strong> { errorMessage }
</p>
</div>
);
}
}
return (
<div>
{ displayMessage() }
</div>
);
}
export default RenderAlert;
Reducers.jsx
import { routerReducer as routing } from 'react-router-redux';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
form: formReducer,
routing
});
export default rootReducer;
use this :
UpdateUserForm = reduxForm({
enableReinitialize: true,
destroyOnUnmount: false,
form: 'update_user_form' // a unique identifier for this form
})(UpdateUserForm);
UpdateUserForm = connect(
(state) => ({
initialValues: state.userManagment.userSingle
})
)(UpdateUserForm);
export default UpdateUserForm;